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
boltz2_client/cli.py
ADDED
|
@@ -0,0 +1,1213 @@
|
|
|
1
|
+
|
|
2
|
+
# ---------------------------------------------------------------
|
|
3
|
+
# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
|
|
4
|
+
# ---------------------------------------------------------------
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
Command-line interface for Boltz-2 Python Client.
|
|
8
|
+
|
|
9
|
+
This module provides a comprehensive CLI for all Boltz-2 features including
|
|
10
|
+
protein structure prediction, protein-ligand complexes, covalent complexes,
|
|
11
|
+
DNA-protein complexes, and advanced parameter control.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import json
|
|
16
|
+
import sys
|
|
17
|
+
import time
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import List, Optional, Tuple, Dict, Any
|
|
20
|
+
|
|
21
|
+
import click
|
|
22
|
+
from rich.console import Console
|
|
23
|
+
from rich.table import Table
|
|
24
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
|
|
25
|
+
from rich.panel import Panel
|
|
26
|
+
from rich.text import Text
|
|
27
|
+
import yaml as pyyaml
|
|
28
|
+
|
|
29
|
+
from .client import Boltz2Client, Boltz2SyncClient, EndpointType
|
|
30
|
+
from .models import (
|
|
31
|
+
PredictionRequest, Polymer, Ligand, PocketConstraint, BondConstraint,
|
|
32
|
+
Atom, AlignmentFileRecord, AlignmentFormat
|
|
33
|
+
)
|
|
34
|
+
from .exceptions import Boltz2ClientError
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
console = Console()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def print_success(message: str):
|
|
41
|
+
"""Print success message."""
|
|
42
|
+
console.print(f"✅ {message}", style="green")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def print_error(message: str):
|
|
46
|
+
"""Print error message."""
|
|
47
|
+
console.print(f"❌ {message}", style="red")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def print_info(message: str):
|
|
51
|
+
"""Print info message."""
|
|
52
|
+
console.print(f"ℹ️ {message}", style="blue")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def print_warning(message: str):
|
|
56
|
+
"""Print warning message."""
|
|
57
|
+
console.print(f"⚠️ {message}", style="yellow")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@click.group()
|
|
61
|
+
@click.option('--base-url', default='http://localhost:8000', help='Service base URL')
|
|
62
|
+
@click.option('--api-key', help='API key for NVIDIA hosted endpoints (or set NVIDIA_API_KEY env var)')
|
|
63
|
+
@click.option('--endpoint-type',
|
|
64
|
+
type=click.Choice(['local', 'nvidia_hosted']),
|
|
65
|
+
default='local',
|
|
66
|
+
help='Type of endpoint: local or nvidia_hosted')
|
|
67
|
+
@click.option('--timeout', default=300.0, help='Request timeout in seconds')
|
|
68
|
+
@click.option('--poll-seconds', default=10, help='Polling interval for NVIDIA hosted endpoints')
|
|
69
|
+
@click.option('--verbose', '-v', is_flag=True, help='Enable verbose output')
|
|
70
|
+
@click.pass_context
|
|
71
|
+
def cli(ctx, base_url: str, api_key: Optional[str], endpoint_type: str,
|
|
72
|
+
timeout: float, poll_seconds: int, verbose: bool):
|
|
73
|
+
"""
|
|
74
|
+
Boltz-2 Python Client CLI
|
|
75
|
+
|
|
76
|
+
Supports both local deployments and NVIDIA hosted endpoints.
|
|
77
|
+
|
|
78
|
+
Examples:
|
|
79
|
+
|
|
80
|
+
# Local endpoint
|
|
81
|
+
boltz2 --base-url http://localhost:8000 protein "MKTVRQERLK..."
|
|
82
|
+
|
|
83
|
+
# NVIDIA hosted endpoint
|
|
84
|
+
boltz2 --base-url https://health.api.nvidia.com --endpoint-type nvidia_hosted --api-key YOUR_KEY protein "MKTVRQERLK..."
|
|
85
|
+
|
|
86
|
+
# Using environment variable for API key
|
|
87
|
+
export NVIDIA_API_KEY=your_api_key
|
|
88
|
+
boltz2 --base-url https://health.api.nvidia.com --endpoint-type nvidia_hosted protein "MKTVRQERLK..."
|
|
89
|
+
"""
|
|
90
|
+
ctx.ensure_object(dict)
|
|
91
|
+
ctx.obj['base_url'] = base_url
|
|
92
|
+
ctx.obj['api_key'] = api_key
|
|
93
|
+
ctx.obj['endpoint_type'] = endpoint_type
|
|
94
|
+
ctx.obj['timeout'] = timeout
|
|
95
|
+
ctx.obj['poll_seconds'] = poll_seconds
|
|
96
|
+
ctx.obj['verbose'] = verbose
|
|
97
|
+
|
|
98
|
+
if verbose:
|
|
99
|
+
print_info(f"Using {endpoint_type} endpoint: {base_url}")
|
|
100
|
+
if endpoint_type == 'nvidia_hosted':
|
|
101
|
+
if api_key:
|
|
102
|
+
print_info("API key provided via command line")
|
|
103
|
+
else:
|
|
104
|
+
print_info("API key will be read from NVIDIA_API_KEY environment variable")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def create_client(ctx) -> Boltz2Client:
|
|
108
|
+
"""Create a Boltz2Client from context."""
|
|
109
|
+
return Boltz2Client(
|
|
110
|
+
base_url=ctx.obj['base_url'],
|
|
111
|
+
api_key=ctx.obj['api_key'],
|
|
112
|
+
endpoint_type=ctx.obj['endpoint_type'],
|
|
113
|
+
timeout=ctx.obj['timeout'],
|
|
114
|
+
poll_seconds=ctx.obj['poll_seconds'],
|
|
115
|
+
console=console
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@cli.command()
|
|
120
|
+
@click.pass_context
|
|
121
|
+
def health(ctx):
|
|
122
|
+
"""Check the health status of the Boltz-2 service."""
|
|
123
|
+
async def check_health():
|
|
124
|
+
try:
|
|
125
|
+
# Handle NVIDIA hosted endpoints specially
|
|
126
|
+
if ctx.obj['endpoint_type'] == 'nvidia_hosted':
|
|
127
|
+
print_warning("Health checks are not supported on NVIDIA hosted endpoints")
|
|
128
|
+
print_info("NVIDIA hosted endpoints use managed infrastructure with built-in health monitoring")
|
|
129
|
+
print_success("NVIDIA endpoint is considered healthy if you can make predictions")
|
|
130
|
+
|
|
131
|
+
if ctx.obj['verbose']:
|
|
132
|
+
console.print("\nService Info:", style="bold")
|
|
133
|
+
console.print(f" Base URL: {ctx.obj['base_url']}")
|
|
134
|
+
console.print(f" Endpoint Type: {ctx.obj['endpoint_type']}")
|
|
135
|
+
console.print(f" API Key: {'✅ Set via environment' if ctx.obj.get('api_key') is None else '✅ Provided via CLI'}")
|
|
136
|
+
console.print(f" Note: To verify connectivity, try running a prediction command")
|
|
137
|
+
|
|
138
|
+
print_info("To test connectivity, try: boltz2 --endpoint-type nvidia_hosted protein \"SEQUENCE\" --no-save")
|
|
139
|
+
else:
|
|
140
|
+
# Local endpoint - use normal health check
|
|
141
|
+
client = create_client(ctx)
|
|
142
|
+
|
|
143
|
+
with Progress(
|
|
144
|
+
SpinnerColumn(),
|
|
145
|
+
TextColumn("[progress.description]{task.description}"),
|
|
146
|
+
console=console
|
|
147
|
+
) as progress:
|
|
148
|
+
task = progress.add_task("Checking service health...", total=None)
|
|
149
|
+
|
|
150
|
+
health_status = await client.health_check()
|
|
151
|
+
progress.remove_task(task)
|
|
152
|
+
|
|
153
|
+
if health_status.status == "healthy":
|
|
154
|
+
print_success(f"Service is healthy (Status: {health_status.status})")
|
|
155
|
+
else:
|
|
156
|
+
print_warning(f"Service status: {health_status.status}")
|
|
157
|
+
|
|
158
|
+
if ctx.obj['verbose'] and health_status.details:
|
|
159
|
+
console.print("\nDetails:", style="bold")
|
|
160
|
+
for key, value in health_status.details.items():
|
|
161
|
+
console.print(f" {key}: {value}")
|
|
162
|
+
|
|
163
|
+
except Exception as e:
|
|
164
|
+
if ctx.obj['endpoint_type'] != 'nvidia_hosted':
|
|
165
|
+
print_error(f"Health check failed: {e}")
|
|
166
|
+
raise click.Abort()
|
|
167
|
+
|
|
168
|
+
asyncio.run(check_health())
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@cli.command()
|
|
172
|
+
@click.pass_context
|
|
173
|
+
def metadata(ctx):
|
|
174
|
+
"""Get service metadata and model information."""
|
|
175
|
+
async def get_metadata():
|
|
176
|
+
try:
|
|
177
|
+
client = create_client(ctx)
|
|
178
|
+
|
|
179
|
+
with Progress(
|
|
180
|
+
SpinnerColumn(),
|
|
181
|
+
TextColumn("[progress.description]{task.description}"),
|
|
182
|
+
console=console
|
|
183
|
+
) as progress:
|
|
184
|
+
task = progress.add_task("Fetching service metadata...", total=None)
|
|
185
|
+
|
|
186
|
+
metadata = await client.get_service_metadata()
|
|
187
|
+
progress.remove_task(task)
|
|
188
|
+
|
|
189
|
+
print_success("Service metadata retrieved successfully")
|
|
190
|
+
|
|
191
|
+
# Display metadata in a nice table
|
|
192
|
+
table = Table(title="Service Metadata")
|
|
193
|
+
table.add_column("Property", style="cyan", no_wrap=True)
|
|
194
|
+
table.add_column("Value", style="magenta")
|
|
195
|
+
|
|
196
|
+
table.add_row("Version", metadata.version)
|
|
197
|
+
table.add_row("Repository Override", metadata.repository_override)
|
|
198
|
+
table.add_row("Asset Info", ", ".join(metadata.assetInfo))
|
|
199
|
+
|
|
200
|
+
if metadata.modelInfo:
|
|
201
|
+
for i, model in enumerate(metadata.modelInfo):
|
|
202
|
+
table.add_row(f"Model {i+1} URL", model.modelUrl)
|
|
203
|
+
table.add_row(f"Model {i+1} Name", model.shortName)
|
|
204
|
+
|
|
205
|
+
console.print(table)
|
|
206
|
+
|
|
207
|
+
except Exception as e:
|
|
208
|
+
print_error(f"Failed to get metadata: {e}")
|
|
209
|
+
raise click.Abort()
|
|
210
|
+
|
|
211
|
+
asyncio.run(get_metadata())
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@cli.command()
|
|
215
|
+
@click.argument('sequence')
|
|
216
|
+
@click.option('--polymer-id', default='A', help='Polymer identifier (default: A)')
|
|
217
|
+
@click.option('--recycling-steps', default=3, type=click.IntRange(1, 6),
|
|
218
|
+
help='Number of recycling steps (1-6, default: 3)')
|
|
219
|
+
@click.option('--sampling-steps', default=50, type=click.IntRange(10, 1000),
|
|
220
|
+
help='Number of sampling steps (10-1000, default: 50)')
|
|
221
|
+
@click.option('--diffusion-samples', default=1, type=click.IntRange(1, 5),
|
|
222
|
+
help='Number of diffusion samples (1-5, default: 1)')
|
|
223
|
+
@click.option('--step-scale', default=1.638, type=click.FloatRange(0.5, 5.0),
|
|
224
|
+
help='Step scale for diffusion sampling (0.5-5.0, default: 1.638)')
|
|
225
|
+
@click.option('--msa-file', multiple=True, type=(str, click.Choice(['sto', 'a3m', 'csv', 'fasta'])),
|
|
226
|
+
help='MSA file and format (can be specified multiple times)')
|
|
227
|
+
@click.option('--output-dir', type=click.Path(), default='.', help='Directory to save output files (structure_0.cif, prediction_metadata.json). Default: current directory')
|
|
228
|
+
@click.option('--no-save', is_flag=True, help='Do not save structure files')
|
|
229
|
+
@click.pass_context
|
|
230
|
+
def protein(ctx, sequence: str, polymer_id: str, recycling_steps: int, sampling_steps: int,
|
|
231
|
+
diffusion_samples: int, step_scale: float, msa_file: List[Tuple[str, str]],
|
|
232
|
+
output_dir: str, no_save: bool):
|
|
233
|
+
"""
|
|
234
|
+
Predict protein structure with optional MSA guidance.
|
|
235
|
+
|
|
236
|
+
SEQUENCE: Protein amino acid sequence
|
|
237
|
+
|
|
238
|
+
Examples:
|
|
239
|
+
boltz2 protein "MKTVRQERLKSIVRILERSKEPVSGAQLAEELSVSRQVIVQDIAYLRSLGYNIVATPRGYVLAGG"
|
|
240
|
+
boltz2 protein "SEQUENCE" --msa-file alignment.a3m a3m --recycling-steps 5
|
|
241
|
+
boltz2 protein "SEQUENCE" --output-dir ./results --sampling-steps 100
|
|
242
|
+
"""
|
|
243
|
+
async def run_protein_prediction():
|
|
244
|
+
try:
|
|
245
|
+
client = create_client(ctx)
|
|
246
|
+
|
|
247
|
+
# Prepare MSA files
|
|
248
|
+
msa_files = []
|
|
249
|
+
for file_path, format_type in msa_file:
|
|
250
|
+
if not Path(file_path).exists():
|
|
251
|
+
print_error(f"MSA file not found: {file_path}")
|
|
252
|
+
raise click.Abort()
|
|
253
|
+
msa_files.append((file_path, format_type))
|
|
254
|
+
|
|
255
|
+
print_info(f"Predicting structure for protein sequence (length: {len(sequence)})")
|
|
256
|
+
print_info(f"Parameters: recycling_steps={recycling_steps}, sampling_steps={sampling_steps}")
|
|
257
|
+
print_info(f" diffusion_samples={diffusion_samples}, step_scale={step_scale}")
|
|
258
|
+
|
|
259
|
+
if msa_files:
|
|
260
|
+
print_info(f"Using {len(msa_files)} MSA file(s)")
|
|
261
|
+
|
|
262
|
+
with Progress(
|
|
263
|
+
SpinnerColumn(),
|
|
264
|
+
TextColumn("[progress.description]{task.description}"),
|
|
265
|
+
TimeElapsedColumn(),
|
|
266
|
+
console=console,
|
|
267
|
+
) as progress:
|
|
268
|
+
task = progress.add_task("Making prediction...", total=None)
|
|
269
|
+
|
|
270
|
+
def progress_callback(message: str):
|
|
271
|
+
progress.update(task, description=message)
|
|
272
|
+
|
|
273
|
+
result = await client.predict_protein_structure(
|
|
274
|
+
sequence=sequence,
|
|
275
|
+
polymer_id=polymer_id,
|
|
276
|
+
recycling_steps=recycling_steps,
|
|
277
|
+
sampling_steps=sampling_steps,
|
|
278
|
+
diffusion_samples=diffusion_samples,
|
|
279
|
+
step_scale=step_scale,
|
|
280
|
+
msa_files=msa_files if msa_files else None,
|
|
281
|
+
save_structures=not no_save,
|
|
282
|
+
output_dir=Path(output_dir),
|
|
283
|
+
progress_callback=progress_callback
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
progress.update(task, description="Prediction completed!")
|
|
287
|
+
|
|
288
|
+
# Display results
|
|
289
|
+
print_success(f"Prediction completed successfully!")
|
|
290
|
+
print_info(f"Generated {len(result.structures)} structure(s)")
|
|
291
|
+
|
|
292
|
+
if result.confidence_scores:
|
|
293
|
+
avg_confidence = sum(result.confidence_scores) / len(result.confidence_scores)
|
|
294
|
+
print_info(f"Average confidence: {avg_confidence:.3f}")
|
|
295
|
+
|
|
296
|
+
if not no_save:
|
|
297
|
+
print_info(f"Structures saved to: {output_dir}")
|
|
298
|
+
|
|
299
|
+
except Exception as e:
|
|
300
|
+
print_error(f"Prediction failed: {e}")
|
|
301
|
+
raise click.Abort()
|
|
302
|
+
|
|
303
|
+
asyncio.run(run_protein_prediction())
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@cli.command()
|
|
307
|
+
@click.argument('protein_sequence')
|
|
308
|
+
@click.option('--smiles', help='Ligand SMILES string')
|
|
309
|
+
@click.option('--ccd', help='Ligand CCD code (alternative to SMILES)')
|
|
310
|
+
@click.option('--protein-id', default='A', help='Protein identifier (default: A)')
|
|
311
|
+
@click.option('--ligand-id', default='LIG', help='Ligand identifier (default: LIG)')
|
|
312
|
+
@click.option('--pocket-residues', help='Comma-separated list of pocket residue indices')
|
|
313
|
+
@click.option('--recycling-steps', default=3, type=click.IntRange(1, 6))
|
|
314
|
+
@click.option('--sampling-steps', default=50, type=click.IntRange(10, 1000))
|
|
315
|
+
@click.option('--predict-affinity', is_flag=True, help='Enable affinity prediction for the ligand')
|
|
316
|
+
@click.option('--sampling-steps-affinity', default=200, type=click.IntRange(10, 1000), help='Sampling steps for affinity prediction (default: 200)')
|
|
317
|
+
@click.option('--diffusion-samples-affinity', default=5, type=click.IntRange(1, 10), help='Diffusion samples for affinity prediction (default: 5)')
|
|
318
|
+
@click.option('--affinity-mw-correction', is_flag=True, help='Apply molecular weight correction to affinity prediction')
|
|
319
|
+
@click.option('--output-dir', type=click.Path(), default='.', help='Directory to save output files (structure_0.cif, prediction_metadata.json). Default: current directory')
|
|
320
|
+
@click.option('--no-save', is_flag=True, help='Do not save structure files')
|
|
321
|
+
@click.pass_context
|
|
322
|
+
def ligand(ctx, protein_sequence: str, smiles: Optional[str], ccd: Optional[str],
|
|
323
|
+
protein_id: str, ligand_id: str, pocket_residues: Optional[str],
|
|
324
|
+
recycling_steps: int, sampling_steps: int, predict_affinity: bool,
|
|
325
|
+
sampling_steps_affinity: int, diffusion_samples_affinity: int,
|
|
326
|
+
affinity_mw_correction: bool, output_dir: str, no_save: bool):
|
|
327
|
+
"""
|
|
328
|
+
Predict protein-ligand complex structure.
|
|
329
|
+
|
|
330
|
+
PROTEIN_SEQUENCE: Protein amino acid sequence
|
|
331
|
+
|
|
332
|
+
Example:
|
|
333
|
+
boltz2 ligand "PROTEIN_SEQ" --smiles "CC(=O)OC1=CC=CC=C1C(=O)O"
|
|
334
|
+
boltz2 ligand "PROTEIN_SEQ" --ccd ASP --pocket-residues "10,15,20,25"
|
|
335
|
+
"""
|
|
336
|
+
if not smiles and not ccd:
|
|
337
|
+
print_error("Must provide either --smiles or --ccd")
|
|
338
|
+
raise click.Abort()
|
|
339
|
+
|
|
340
|
+
if smiles and ccd:
|
|
341
|
+
print_error("Cannot specify both --smiles and --ccd")
|
|
342
|
+
raise click.Abort()
|
|
343
|
+
|
|
344
|
+
async def run_ligand_prediction():
|
|
345
|
+
try:
|
|
346
|
+
client = create_client(ctx)
|
|
347
|
+
|
|
348
|
+
# Parse pocket residues
|
|
349
|
+
pocket_residue_list = None
|
|
350
|
+
if pocket_residues:
|
|
351
|
+
pocket_residue_list = [int(x.strip()) for x in pocket_residues.split(',')]
|
|
352
|
+
|
|
353
|
+
print_info(f"Predicting protein-ligand complex")
|
|
354
|
+
print_info(f"Protein length: {len(protein_sequence)}")
|
|
355
|
+
print_info(f"Ligand: {smiles or ccd}")
|
|
356
|
+
|
|
357
|
+
if pocket_residue_list:
|
|
358
|
+
print_info(f"Pocket residues: {pocket_residue_list}")
|
|
359
|
+
|
|
360
|
+
if predict_affinity:
|
|
361
|
+
print_info(f"Affinity prediction: ENABLED")
|
|
362
|
+
print_info(f" - Sampling steps: {sampling_steps_affinity}")
|
|
363
|
+
print_info(f" - Diffusion samples: {diffusion_samples_affinity}")
|
|
364
|
+
print_info(f" - MW correction: {affinity_mw_correction}")
|
|
365
|
+
|
|
366
|
+
with Progress(
|
|
367
|
+
SpinnerColumn(),
|
|
368
|
+
TextColumn("[progress.description]{task.description}"),
|
|
369
|
+
TimeElapsedColumn(),
|
|
370
|
+
console=console,
|
|
371
|
+
) as progress:
|
|
372
|
+
task = progress.add_task("Making prediction...", total=None)
|
|
373
|
+
|
|
374
|
+
def progress_callback(message: str):
|
|
375
|
+
progress.update(task, description=message)
|
|
376
|
+
|
|
377
|
+
# Create request with affinity parameters
|
|
378
|
+
polymer = Polymer(
|
|
379
|
+
id=protein_id,
|
|
380
|
+
molecule_type="protein",
|
|
381
|
+
sequence=protein_sequence
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
ligand_obj = Ligand(
|
|
385
|
+
id=ligand_id,
|
|
386
|
+
smiles=smiles,
|
|
387
|
+
ccd=ccd,
|
|
388
|
+
predict_affinity=predict_affinity
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
request = PredictionRequest(
|
|
392
|
+
polymers=[polymer],
|
|
393
|
+
ligands=[ligand_obj],
|
|
394
|
+
recycling_steps=recycling_steps,
|
|
395
|
+
sampling_steps=sampling_steps,
|
|
396
|
+
sampling_steps_affinity=sampling_steps_affinity if predict_affinity else None,
|
|
397
|
+
diffusion_samples_affinity=diffusion_samples_affinity if predict_affinity else None,
|
|
398
|
+
affinity_mw_correction=affinity_mw_correction if predict_affinity else None
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
result = await client.predict(request)
|
|
402
|
+
|
|
403
|
+
progress.update(task, description="Prediction completed!")
|
|
404
|
+
|
|
405
|
+
print_success(f"Complex prediction completed successfully!")
|
|
406
|
+
print_info(f"Generated {len(result.structures)} structure(s)")
|
|
407
|
+
|
|
408
|
+
if result.confidence_scores:
|
|
409
|
+
avg_confidence = sum(result.confidence_scores) / len(result.confidence_scores)
|
|
410
|
+
print_info(f"Average confidence: {avg_confidence:.3f}")
|
|
411
|
+
|
|
412
|
+
# Display affinity results if available
|
|
413
|
+
if predict_affinity and result.affinities and ligand_id in result.affinities:
|
|
414
|
+
console.print("\n📊 Affinity Prediction Results:", style="bold cyan")
|
|
415
|
+
affinity = result.affinities[ligand_id]
|
|
416
|
+
|
|
417
|
+
table = Table(show_header=True, header_style="bold magenta")
|
|
418
|
+
table.add_column("Metric", style="cyan", no_wrap=True)
|
|
419
|
+
table.add_column("Value", style="green")
|
|
420
|
+
|
|
421
|
+
table.add_row("pIC50", f"{affinity.affinity_pic50[0]:.3f}")
|
|
422
|
+
table.add_row("log(IC50)", f"{affinity.affinity_pred_value[0]:.3f}")
|
|
423
|
+
table.add_row("Binding Probability", f"{affinity.affinity_probability_binary[0]:.3f}")
|
|
424
|
+
|
|
425
|
+
# pIC50 = -log10(IC50 in M), so IC50 in M = 10^(-pIC50)
|
|
426
|
+
ic50_nm = 10 ** (-affinity.affinity_pic50[0]) * 1e9
|
|
427
|
+
table.add_row("Estimated IC50", f"{ic50_nm:.2f} nM")
|
|
428
|
+
|
|
429
|
+
console.print(table)
|
|
430
|
+
|
|
431
|
+
# Interpretation
|
|
432
|
+
if affinity.affinity_probability_binary[0] > 0.7:
|
|
433
|
+
print_success("Strong binding predicted (>70% probability)")
|
|
434
|
+
elif affinity.affinity_probability_binary[0] > 0.5:
|
|
435
|
+
print_info("Moderate binding predicted (>50% probability)")
|
|
436
|
+
else:
|
|
437
|
+
print_info("Weak binding predicted (<50% probability)")
|
|
438
|
+
|
|
439
|
+
# Save results
|
|
440
|
+
if not no_save:
|
|
441
|
+
output_path = Path(output_dir)
|
|
442
|
+
output_path.mkdir(exist_ok=True)
|
|
443
|
+
|
|
444
|
+
# Save structure
|
|
445
|
+
structure_file = output_path / "structure_0.cif"
|
|
446
|
+
with open(structure_file, 'w') as f:
|
|
447
|
+
f.write(result.structures[0].structure)
|
|
448
|
+
print_info(f"Structure saved to: {structure_file}")
|
|
449
|
+
|
|
450
|
+
# Save affinity results if available
|
|
451
|
+
if predict_affinity and result.affinities and ligand_id in result.affinities:
|
|
452
|
+
affinity_file = output_path / "affinity_results.json"
|
|
453
|
+
affinity_data = {
|
|
454
|
+
"ligand_id": ligand_id,
|
|
455
|
+
"ligand": smiles or ccd,
|
|
456
|
+
"predictions": {
|
|
457
|
+
"log_ic50": affinity.affinity_pred_value[0],
|
|
458
|
+
"pic50": affinity.affinity_pic50[0],
|
|
459
|
+
"binding_probability": affinity.affinity_probability_binary[0],
|
|
460
|
+
"ic50_nm": ic50_nm
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
with open(affinity_file, 'w') as f:
|
|
464
|
+
json.dump(affinity_data, f, indent=2)
|
|
465
|
+
print_info(f"Affinity results saved to: {affinity_file}")
|
|
466
|
+
|
|
467
|
+
except Exception as e:
|
|
468
|
+
print_error(f"Prediction failed: {e}")
|
|
469
|
+
raise click.Abort()
|
|
470
|
+
|
|
471
|
+
asyncio.run(run_ligand_prediction())
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
@cli.command()
|
|
475
|
+
@click.argument('protein_sequence')
|
|
476
|
+
@click.option('--ccd', help='Ligand CCD code (required for covalent bonding)')
|
|
477
|
+
@click.option('--bond', 'bonds', multiple=True,
|
|
478
|
+
help='Bond constraint: POLYMER_ID:RESIDUE_INDEX:ATOM_NAME:LIGAND_ID:ATOM_NAME (can be specified multiple times)')
|
|
479
|
+
@click.option('--disulfide', 'disulfides', multiple=True,
|
|
480
|
+
help='Disulfide bond: POLYMER_ID:RESIDUE1_INDEX:POLYMER_ID:RESIDUE2_INDEX (can be specified multiple times)')
|
|
481
|
+
@click.option('--protein-id', default='A', help='Protein identifier (default: A)')
|
|
482
|
+
@click.option('--ligand-id', default='LIG', help='Ligand identifier (default: LIG)')
|
|
483
|
+
@click.option('--recycling-steps', default=3, type=click.IntRange(1, 6))
|
|
484
|
+
@click.option('--sampling-steps', default=50, type=click.IntRange(10, 1000))
|
|
485
|
+
@click.option('--output-dir', type=click.Path(), default='.', help='Directory to save output files (structure_0.cif, prediction_metadata.json). Default: current directory')
|
|
486
|
+
@click.option('--no-save', is_flag=True, help='Do not save structure files')
|
|
487
|
+
@click.pass_context
|
|
488
|
+
def covalent(ctx, protein_sequence: str, ccd: Optional[str],
|
|
489
|
+
bonds: List[str], disulfides: List[str], protein_id: str, ligand_id: str,
|
|
490
|
+
recycling_steps: int, sampling_steps: int, output_dir: str, no_save: bool):
|
|
491
|
+
"""
|
|
492
|
+
Predict covalent complex structure with flexible bond constraints.
|
|
493
|
+
|
|
494
|
+
Note: Covalent bonding only supports CCD codes for ligands, not SMILES.
|
|
495
|
+
|
|
496
|
+
This command supports various types of covalent bonds:
|
|
497
|
+
|
|
498
|
+
\b
|
|
499
|
+
1. Protein-Ligand bonds (requires --ccd):
|
|
500
|
+
--bond A:12:SG:LIG:C22 (Cys12 SG to ligand C22)
|
|
501
|
+
--bond A:45:NE2:LIG:C1 (His45 NE2 to ligand C1)
|
|
502
|
+
|
|
503
|
+
\b
|
|
504
|
+
2. Disulfide bonds (protein-only, no ligand needed):
|
|
505
|
+
--disulfide A:12:A:45 (Cys12 to Cys45 in same chain)
|
|
506
|
+
--disulfide A:12:B:23 (Cys12 in chain A to Cys23 in chain B)
|
|
507
|
+
|
|
508
|
+
\b
|
|
509
|
+
3. Multiple bonds:
|
|
510
|
+
--bond A:12:SG:LIG:C22 --bond A:45:NE2:LIG:C1
|
|
511
|
+
|
|
512
|
+
Examples:
|
|
513
|
+
|
|
514
|
+
\b
|
|
515
|
+
# Covalent protein-ligand complex (CCD required)
|
|
516
|
+
boltz2 covalent "MKTVRQERLKCSIVRIL..." --ccd U4U --bond A:12:SG:LIG:C22
|
|
517
|
+
|
|
518
|
+
\b
|
|
519
|
+
# Disulfide bond in protein (no ligand needed)
|
|
520
|
+
boltz2 covalent "MKTVRQERLKCSIVRILCSKEPVSGAQLAEELSVSRQVIVQDIAYLRSLGYNIVATPRGYVLAGG" --disulfide A:12:A:25
|
|
521
|
+
|
|
522
|
+
\b
|
|
523
|
+
# Multiple covalent bonds with ligand
|
|
524
|
+
boltz2 covalent "SEQUENCE..." --ccd ATP --bond A:12:SG:LIG:C22 --bond A:45:NE2:LIG:C1
|
|
525
|
+
"""
|
|
526
|
+
async def run_covalent_prediction():
|
|
527
|
+
try:
|
|
528
|
+
client = create_client(ctx)
|
|
529
|
+
|
|
530
|
+
# Validate inputs
|
|
531
|
+
if not bonds and not disulfides:
|
|
532
|
+
print_error("At least one bond constraint (--bond or --disulfide) must be specified")
|
|
533
|
+
raise click.Abort()
|
|
534
|
+
|
|
535
|
+
if bonds and not ccd:
|
|
536
|
+
print_error("CCD code (--ccd) is required when using --bond constraints")
|
|
537
|
+
print_error("Note: Covalent bonding only supports CCD codes, not SMILES")
|
|
538
|
+
raise click.Abort()
|
|
539
|
+
|
|
540
|
+
# Parse bond constraints
|
|
541
|
+
bond_constraints = []
|
|
542
|
+
|
|
543
|
+
# Parse protein-ligand bonds
|
|
544
|
+
for bond_spec in bonds:
|
|
545
|
+
try:
|
|
546
|
+
parts = bond_spec.split(':')
|
|
547
|
+
if len(parts) != 5:
|
|
548
|
+
raise ValueError("Bond format: POLYMER_ID:RESIDUE_INDEX:ATOM_NAME:LIGAND_ID:ATOM_NAME")
|
|
549
|
+
|
|
550
|
+
polymer_id, residue_idx, protein_atom, lig_id, ligand_atom = parts
|
|
551
|
+
residue_idx = int(residue_idx)
|
|
552
|
+
|
|
553
|
+
bond_constraint = BondConstraint(
|
|
554
|
+
constraint_type="bond",
|
|
555
|
+
atoms=[
|
|
556
|
+
Atom(id=polymer_id, residue_index=residue_idx, atom_name=protein_atom),
|
|
557
|
+
Atom(id=lig_id, residue_index=1, atom_name=ligand_atom)
|
|
558
|
+
]
|
|
559
|
+
)
|
|
560
|
+
bond_constraints.append(bond_constraint)
|
|
561
|
+
|
|
562
|
+
except (ValueError, IndexError) as e:
|
|
563
|
+
print_error(f"Invalid bond specification '{bond_spec}': {e}")
|
|
564
|
+
raise click.Abort()
|
|
565
|
+
|
|
566
|
+
# Parse disulfide bonds
|
|
567
|
+
for disulfide_spec in disulfides:
|
|
568
|
+
try:
|
|
569
|
+
parts = disulfide_spec.split(':')
|
|
570
|
+
if len(parts) != 4:
|
|
571
|
+
raise ValueError("Disulfide format: POLYMER_ID1:RESIDUE1_INDEX:POLYMER_ID2:RESIDUE2_INDEX")
|
|
572
|
+
|
|
573
|
+
polymer1_id, residue1_idx, polymer2_id, residue2_idx = parts
|
|
574
|
+
residue1_idx = int(residue1_idx)
|
|
575
|
+
residue2_idx = int(residue2_idx)
|
|
576
|
+
|
|
577
|
+
bond_constraint = BondConstraint(
|
|
578
|
+
constraint_type="bond",
|
|
579
|
+
atoms=[
|
|
580
|
+
Atom(id=polymer1_id, residue_index=residue1_idx, atom_name="SG"),
|
|
581
|
+
Atom(id=polymer2_id, residue_index=residue2_idx, atom_name="SG")
|
|
582
|
+
]
|
|
583
|
+
)
|
|
584
|
+
bond_constraints.append(bond_constraint)
|
|
585
|
+
|
|
586
|
+
except (ValueError, IndexError) as e:
|
|
587
|
+
print_error(f"Invalid disulfide specification '{disulfide_spec}': {e}")
|
|
588
|
+
raise click.Abort()
|
|
589
|
+
|
|
590
|
+
# Create polymers
|
|
591
|
+
polymers = [Polymer(
|
|
592
|
+
id=protein_id,
|
|
593
|
+
molecule_type="protein",
|
|
594
|
+
sequence=protein_sequence
|
|
595
|
+
)]
|
|
596
|
+
|
|
597
|
+
# Create ligands if specified
|
|
598
|
+
ligands = []
|
|
599
|
+
if ccd:
|
|
600
|
+
ligand = Ligand(
|
|
601
|
+
id=ligand_id,
|
|
602
|
+
ccd=ccd
|
|
603
|
+
)
|
|
604
|
+
ligands.append(ligand)
|
|
605
|
+
|
|
606
|
+
# Display prediction info
|
|
607
|
+
print_info("Predicting covalent complex structure")
|
|
608
|
+
print_info(f"Protein length: {len(protein_sequence)}")
|
|
609
|
+
if ccd:
|
|
610
|
+
print_info(f"Ligand CCD: {ccd}")
|
|
611
|
+
|
|
612
|
+
print_info(f"Bond constraints: {len(bond_constraints)}")
|
|
613
|
+
for i, constraint in enumerate(bond_constraints, 1):
|
|
614
|
+
atom1, atom2 = constraint.atoms
|
|
615
|
+
print_info(f" {i}. {atom1.id}:{atom1.residue_index}:{atom1.atom_name} ↔ {atom2.id}:{atom2.residue_index}:{atom2.atom_name}")
|
|
616
|
+
|
|
617
|
+
with Progress(
|
|
618
|
+
SpinnerColumn(),
|
|
619
|
+
TextColumn("[progress.description]{task.description}"),
|
|
620
|
+
TimeElapsedColumn(),
|
|
621
|
+
console=console,
|
|
622
|
+
) as progress:
|
|
623
|
+
task = progress.add_task("Predicting covalent complex...", total=None)
|
|
624
|
+
|
|
625
|
+
def progress_callback(message: str):
|
|
626
|
+
progress.update(task, description=message)
|
|
627
|
+
|
|
628
|
+
response = await client.predict_with_advanced_parameters(
|
|
629
|
+
polymers=polymers,
|
|
630
|
+
ligands=ligands if ligands else None,
|
|
631
|
+
constraints=bond_constraints,
|
|
632
|
+
recycling_steps=recycling_steps,
|
|
633
|
+
sampling_steps=sampling_steps,
|
|
634
|
+
save_structures=not no_save,
|
|
635
|
+
output_dir=Path(output_dir),
|
|
636
|
+
progress_callback=progress_callback
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
progress.update(task, description="Prediction completed!")
|
|
640
|
+
|
|
641
|
+
print_success("Covalent complex prediction completed successfully!")
|
|
642
|
+
print_info(f"Generated {len(response.structures)} structure(s)")
|
|
643
|
+
|
|
644
|
+
if response.confidence_scores:
|
|
645
|
+
avg_confidence = sum(response.confidence_scores) / len(response.confidence_scores)
|
|
646
|
+
print_info(f"Average confidence: {avg_confidence:.3f}")
|
|
647
|
+
|
|
648
|
+
if not no_save:
|
|
649
|
+
print_info(f"Structures saved to: {output_dir}")
|
|
650
|
+
|
|
651
|
+
except Exception as e:
|
|
652
|
+
print_error(f"Covalent prediction failed: {e}")
|
|
653
|
+
raise click.Abort()
|
|
654
|
+
|
|
655
|
+
asyncio.run(run_covalent_prediction())
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
@cli.command()
|
|
659
|
+
@click.option('--protein-sequences', required=True, help='Comma-separated protein sequences')
|
|
660
|
+
@click.option('--dna-sequences', required=True, help='Comma-separated DNA sequences')
|
|
661
|
+
@click.option('--protein-ids', help='Comma-separated protein IDs (default: A,B,...)')
|
|
662
|
+
@click.option('--dna-ids', help='Comma-separated DNA IDs (default: C,D,...)')
|
|
663
|
+
@click.option('--recycling-steps', default=3, type=click.IntRange(1, 6))
|
|
664
|
+
@click.option('--sampling-steps', default=50, type=click.IntRange(10, 1000))
|
|
665
|
+
@click.option('--concatenate-msas', is_flag=True, help='Concatenate MSAs for polymers')
|
|
666
|
+
@click.option('--output-dir', type=click.Path(), default='.', help='Directory to save output files (structure_0.cif, prediction_metadata.json). Default: current directory')
|
|
667
|
+
@click.option('--no-save', is_flag=True, help='Do not save structure files')
|
|
668
|
+
@click.pass_context
|
|
669
|
+
def dna_protein(ctx, protein_sequences: str, dna_sequences: str, protein_ids: Optional[str],
|
|
670
|
+
dna_ids: Optional[str], recycling_steps: int, sampling_steps: int,
|
|
671
|
+
concatenate_msas: bool, output_dir: str, no_save: bool):
|
|
672
|
+
"""
|
|
673
|
+
Predict DNA-protein complex structure.
|
|
674
|
+
|
|
675
|
+
Example:
|
|
676
|
+
boltz2 dna-protein --protein-sequences "PROT1,PROT2" --dna-sequences "ATCG,CGTA"
|
|
677
|
+
"""
|
|
678
|
+
async def run_dna_protein_prediction():
|
|
679
|
+
try:
|
|
680
|
+
client = create_client(ctx)
|
|
681
|
+
|
|
682
|
+
# Parse sequences
|
|
683
|
+
protein_seq_list = [seq.strip() for seq in protein_sequences.split(',')]
|
|
684
|
+
dna_seq_list = [seq.strip() for seq in dna_sequences.split(',')]
|
|
685
|
+
|
|
686
|
+
# Parse IDs
|
|
687
|
+
protein_id_list = None
|
|
688
|
+
if protein_ids:
|
|
689
|
+
protein_id_list = [id.strip() for id in protein_ids.split(',')]
|
|
690
|
+
|
|
691
|
+
dna_id_list = None
|
|
692
|
+
if dna_ids:
|
|
693
|
+
dna_id_list = [id.strip() for id in dna_ids.split(',')]
|
|
694
|
+
|
|
695
|
+
print_info(f"Predicting DNA-protein complex")
|
|
696
|
+
print_info(f"Proteins: {len(protein_seq_list)} sequences")
|
|
697
|
+
print_info(f"DNA: {len(dna_seq_list)} sequences")
|
|
698
|
+
print_info(f"Concatenate MSAs: {concatenate_msas}")
|
|
699
|
+
|
|
700
|
+
with Progress(
|
|
701
|
+
SpinnerColumn(),
|
|
702
|
+
TextColumn("[progress.description]{task.description}"),
|
|
703
|
+
TimeElapsedColumn(),
|
|
704
|
+
console=console,
|
|
705
|
+
) as progress:
|
|
706
|
+
task = progress.add_task("Making prediction...", total=None)
|
|
707
|
+
|
|
708
|
+
def progress_callback(message: str):
|
|
709
|
+
progress.update(task, description=message)
|
|
710
|
+
|
|
711
|
+
result = await client.predict_dna_protein_complex(
|
|
712
|
+
protein_sequences=protein_seq_list,
|
|
713
|
+
dna_sequences=dna_seq_list,
|
|
714
|
+
protein_ids=protein_id_list,
|
|
715
|
+
dna_ids=dna_id_list,
|
|
716
|
+
recycling_steps=recycling_steps,
|
|
717
|
+
sampling_steps=sampling_steps,
|
|
718
|
+
concatenate_msas=concatenate_msas,
|
|
719
|
+
save_structures=not no_save,
|
|
720
|
+
output_dir=Path(output_dir),
|
|
721
|
+
progress_callback=progress_callback
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
progress.update(task, description="Prediction completed!")
|
|
725
|
+
|
|
726
|
+
print_success(f"DNA-protein complex prediction completed successfully!")
|
|
727
|
+
print_info(f"Generated {len(result.structures)} structure(s)")
|
|
728
|
+
|
|
729
|
+
if result.confidence_scores:
|
|
730
|
+
avg_confidence = sum(result.confidence_scores) / len(result.confidence_scores)
|
|
731
|
+
print_info(f"Average confidence: {avg_confidence:.3f}")
|
|
732
|
+
|
|
733
|
+
except Exception as e:
|
|
734
|
+
print_error(f"Prediction failed: {e}")
|
|
735
|
+
raise click.Abort()
|
|
736
|
+
|
|
737
|
+
asyncio.run(run_dna_protein_prediction())
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
@cli.command()
|
|
741
|
+
@click.option('--config-file', type=click.Path(exists=True), required=True,
|
|
742
|
+
help='JSON configuration file with complete prediction parameters')
|
|
743
|
+
@click.option('--output-dir', type=click.Path(), default='.', help='Directory to save output files (structure_0.cif, prediction_metadata.json). Default: current directory')
|
|
744
|
+
@click.option('--no-save', is_flag=True, help='Do not save structure files')
|
|
745
|
+
@click.pass_context
|
|
746
|
+
def advanced(ctx, config_file: str, output_dir: str, no_save: bool):
|
|
747
|
+
"""
|
|
748
|
+
Run prediction with advanced parameters from JSON configuration file.
|
|
749
|
+
|
|
750
|
+
The JSON file should contain a complete prediction request with all parameters.
|
|
751
|
+
|
|
752
|
+
Example JSON structure:
|
|
753
|
+
{
|
|
754
|
+
"polymers": [
|
|
755
|
+
{
|
|
756
|
+
"id": "A",
|
|
757
|
+
"molecule_type": "protein",
|
|
758
|
+
"sequence": "MKTVRQERLK..."
|
|
759
|
+
}
|
|
760
|
+
],
|
|
761
|
+
"ligands": [
|
|
762
|
+
{
|
|
763
|
+
"id": "LIG",
|
|
764
|
+
"smiles": "CC(=O)O"
|
|
765
|
+
}
|
|
766
|
+
],
|
|
767
|
+
"recycling_steps": 5,
|
|
768
|
+
"sampling_steps": 100,
|
|
769
|
+
"diffusion_samples": 3
|
|
770
|
+
}
|
|
771
|
+
"""
|
|
772
|
+
async def run_advanced_prediction():
|
|
773
|
+
try:
|
|
774
|
+
client = create_client(ctx)
|
|
775
|
+
|
|
776
|
+
# Load configuration
|
|
777
|
+
config_path = Path(config_file)
|
|
778
|
+
config_data = json.loads(config_path.read_text())
|
|
779
|
+
|
|
780
|
+
print_info(f"Loading configuration from {config_path}")
|
|
781
|
+
|
|
782
|
+
# Create prediction request
|
|
783
|
+
request = PredictionRequest(**config_data)
|
|
784
|
+
|
|
785
|
+
print_info("Running advanced prediction with custom parameters")
|
|
786
|
+
print_info(f"Polymers: {len(request.polymers)}")
|
|
787
|
+
if request.ligands:
|
|
788
|
+
print_info(f"Ligands: {len(request.ligands)}")
|
|
789
|
+
if request.constraints:
|
|
790
|
+
print_info(f"Constraints: {len(request.constraints)}")
|
|
791
|
+
|
|
792
|
+
with Progress(
|
|
793
|
+
SpinnerColumn(),
|
|
794
|
+
TextColumn("[progress.description]{task.description}"),
|
|
795
|
+
TimeElapsedColumn(),
|
|
796
|
+
console=console,
|
|
797
|
+
) as progress:
|
|
798
|
+
def progress_callback(message: str):
|
|
799
|
+
progress.console.print(f"🧬 {message}")
|
|
800
|
+
|
|
801
|
+
task = progress.add_task("Making prediction...", total=None)
|
|
802
|
+
|
|
803
|
+
result = await client.predict(
|
|
804
|
+
request,
|
|
805
|
+
save_structures=not no_save,
|
|
806
|
+
output_dir=Path(output_dir),
|
|
807
|
+
progress_callback=progress_callback
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
progress.update(task, description="Prediction completed!")
|
|
811
|
+
|
|
812
|
+
print_success("Advanced prediction completed successfully!")
|
|
813
|
+
print_info(f"Generated {len(result.structures)} structure(s)")
|
|
814
|
+
|
|
815
|
+
if result.confidence_scores:
|
|
816
|
+
avg_confidence = sum(result.confidence_scores) / len(result.confidence_scores)
|
|
817
|
+
print_info(f"Average confidence: {avg_confidence:.3f}")
|
|
818
|
+
|
|
819
|
+
except Exception as e:
|
|
820
|
+
print_error(f"Advanced prediction failed: {e}")
|
|
821
|
+
raise click.Abort()
|
|
822
|
+
|
|
823
|
+
asyncio.run(run_advanced_prediction())
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
@cli.command(name='yaml')
|
|
827
|
+
@click.argument('yaml_file', type=click.Path(exists=True))
|
|
828
|
+
@click.option('--msa-dir', type=click.Path(), help='Directory containing MSA files (default: same as YAML file)')
|
|
829
|
+
@click.option('--recycling-steps', default=3, type=click.IntRange(1, 6))
|
|
830
|
+
@click.option('--sampling-steps', default=50, type=click.IntRange(10, 1000))
|
|
831
|
+
@click.option('--diffusion-samples', default=1, type=click.IntRange(1, 5))
|
|
832
|
+
@click.option('--step-scale', default=1.638, type=click.FloatRange(0.5, 5.0))
|
|
833
|
+
@click.option('--output-dir', type=click.Path(), default='.', help='Directory to save output files (structure_0.cif, prediction_metadata.json). Default: current directory')
|
|
834
|
+
@click.option('--no-save', is_flag=True, help='Do not save structure files')
|
|
835
|
+
@click.pass_context
|
|
836
|
+
def yaml_config(ctx, yaml_file: str, msa_dir: Optional[str], recycling_steps: int,
|
|
837
|
+
sampling_steps: int, diffusion_samples: int, step_scale: float,
|
|
838
|
+
output_dir: str, no_save: bool):
|
|
839
|
+
"""
|
|
840
|
+
Run prediction from YAML configuration file (official Boltz format).
|
|
841
|
+
|
|
842
|
+
This command supports the official Boltz YAML configuration format as used
|
|
843
|
+
in the original Boltz repository examples.
|
|
844
|
+
|
|
845
|
+
YAML_FILE: Path to YAML configuration file
|
|
846
|
+
|
|
847
|
+
Example YAML format:
|
|
848
|
+
|
|
849
|
+
\b
|
|
850
|
+
version: 1
|
|
851
|
+
sequences:
|
|
852
|
+
- protein:
|
|
853
|
+
id: A
|
|
854
|
+
sequence: "MKTVRQERLK..."
|
|
855
|
+
msa: "protein_A.a3m" # optional
|
|
856
|
+
- ligand:
|
|
857
|
+
id: B
|
|
858
|
+
smiles: "CC(=O)O"
|
|
859
|
+
properties: # optional
|
|
860
|
+
affinity:
|
|
861
|
+
binder: B
|
|
862
|
+
|
|
863
|
+
Examples:
|
|
864
|
+
|
|
865
|
+
\b
|
|
866
|
+
# Basic protein-ligand complex
|
|
867
|
+
boltz2 yaml protein_ligand.yaml
|
|
868
|
+
|
|
869
|
+
\b
|
|
870
|
+
# With custom parameters
|
|
871
|
+
boltz2 yaml complex.yaml --recycling-steps 5 --sampling-steps 100
|
|
872
|
+
|
|
873
|
+
\b
|
|
874
|
+
# With custom MSA directory
|
|
875
|
+
boltz2 yaml config.yaml --msa-dir /path/to/msa/files
|
|
876
|
+
|
|
877
|
+
\b
|
|
878
|
+
# Affinity prediction
|
|
879
|
+
boltz2 yaml my_affinity_config.yaml --diffusion-samples 3
|
|
880
|
+
"""
|
|
881
|
+
async def run_yaml_prediction():
|
|
882
|
+
try:
|
|
883
|
+
client = create_client(ctx)
|
|
884
|
+
|
|
885
|
+
yaml_path = Path(yaml_file)
|
|
886
|
+
print_info(f"Loading YAML configuration from {yaml_path}")
|
|
887
|
+
|
|
888
|
+
# Determine MSA directory
|
|
889
|
+
if msa_dir:
|
|
890
|
+
msa_directory = Path(msa_dir)
|
|
891
|
+
else:
|
|
892
|
+
msa_directory = yaml_path.parent
|
|
893
|
+
|
|
894
|
+
print_info(f"MSA directory: {msa_directory}")
|
|
895
|
+
|
|
896
|
+
with Progress(
|
|
897
|
+
SpinnerColumn(),
|
|
898
|
+
TextColumn("[progress.description]{task.description}"),
|
|
899
|
+
TimeElapsedColumn(),
|
|
900
|
+
console=console,
|
|
901
|
+
) as progress:
|
|
902
|
+
def progress_callback(message: str):
|
|
903
|
+
progress.console.print(f"🧬 {message}")
|
|
904
|
+
|
|
905
|
+
task = progress.add_task("Loading configuration...", total=None)
|
|
906
|
+
|
|
907
|
+
# Load and validate YAML config
|
|
908
|
+
yaml_content = yaml_path.read_text()
|
|
909
|
+
yaml_data = pyyaml.safe_load(yaml_content)
|
|
910
|
+
|
|
911
|
+
from .models import YAMLConfig
|
|
912
|
+
config = YAMLConfig(**yaml_data)
|
|
913
|
+
|
|
914
|
+
# Display configuration info
|
|
915
|
+
protein_count = sum(1 for seq in config.sequences if seq.protein)
|
|
916
|
+
ligand_count = sum(1 for seq in config.sequences if seq.ligand)
|
|
917
|
+
|
|
918
|
+
print_info(f"Configuration loaded successfully")
|
|
919
|
+
print_info(f"Proteins: {protein_count}, Ligands: {ligand_count}")
|
|
920
|
+
|
|
921
|
+
if config.properties and config.properties.affinity:
|
|
922
|
+
print_info(f"Affinity prediction enabled for binder: {config.properties.affinity.binder}")
|
|
923
|
+
|
|
924
|
+
progress.update(task, description="Making prediction...")
|
|
925
|
+
|
|
926
|
+
# Convert config to request
|
|
927
|
+
request = config.to_prediction_request()
|
|
928
|
+
|
|
929
|
+
# Handle MSA files for proteins that reference them
|
|
930
|
+
for i, seq in enumerate(config.sequences):
|
|
931
|
+
if seq.protein and seq.protein.msa and seq.protein.msa != "empty":
|
|
932
|
+
msa_path = msa_directory / seq.protein.msa
|
|
933
|
+
if msa_path.exists():
|
|
934
|
+
msa_content = msa_path.read_text()
|
|
935
|
+
# Determine format from extension
|
|
936
|
+
format_map = {
|
|
937
|
+
'.a3m': 'a3m',
|
|
938
|
+
'.sto': 'sto',
|
|
939
|
+
'.fasta': 'fasta',
|
|
940
|
+
'.csv': 'csv'
|
|
941
|
+
}
|
|
942
|
+
format_type = format_map.get(msa_path.suffix.lower(), 'a3m')
|
|
943
|
+
|
|
944
|
+
from .models import AlignmentFileRecord
|
|
945
|
+
msa_record = AlignmentFileRecord(
|
|
946
|
+
alignment=msa_content,
|
|
947
|
+
format=format_type,
|
|
948
|
+
rank=0
|
|
949
|
+
)
|
|
950
|
+
|
|
951
|
+
# Update the corresponding polymer with MSA
|
|
952
|
+
polymer_idx = sum(1 for s in config.sequences[:i] if s.protein)
|
|
953
|
+
if polymer_idx < len(request.polymers):
|
|
954
|
+
request.polymers[polymer_idx].msa = [msa_record]
|
|
955
|
+
else:
|
|
956
|
+
print_warning(f"MSA file not found: {msa_path}")
|
|
957
|
+
|
|
958
|
+
# Override with CLI parameters
|
|
959
|
+
request.recycling_steps = recycling_steps
|
|
960
|
+
request.sampling_steps = sampling_steps
|
|
961
|
+
request.diffusion_samples = diffusion_samples
|
|
962
|
+
request.step_scale = step_scale
|
|
963
|
+
|
|
964
|
+
result = await client.predict(
|
|
965
|
+
request,
|
|
966
|
+
save_structures=not no_save,
|
|
967
|
+
output_dir=Path(output_dir),
|
|
968
|
+
progress_callback=progress_callback
|
|
969
|
+
)
|
|
970
|
+
|
|
971
|
+
progress.update(task, description="Prediction completed!")
|
|
972
|
+
|
|
973
|
+
print_success("YAML prediction completed successfully!")
|
|
974
|
+
print_info(f"Generated {len(result.structures)} structure(s)")
|
|
975
|
+
|
|
976
|
+
if result.confidence_scores:
|
|
977
|
+
avg_confidence = sum(result.confidence_scores) / len(result.confidence_scores)
|
|
978
|
+
print_info(f"Average confidence: {avg_confidence:.3f}")
|
|
979
|
+
|
|
980
|
+
if not no_save:
|
|
981
|
+
print_info(f"Structures saved to: {output_dir}")
|
|
982
|
+
|
|
983
|
+
except Exception as e:
|
|
984
|
+
print_error(f"YAML prediction failed: {e}")
|
|
985
|
+
raise click.Abort()
|
|
986
|
+
|
|
987
|
+
asyncio.run(run_yaml_prediction())
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
@cli.command(name='screen')
|
|
991
|
+
@click.argument('target_sequence', type=str)
|
|
992
|
+
@click.argument('compounds_file', type=click.Path(exists=True))
|
|
993
|
+
@click.option('--target-name', default='Target', help='Name of the target protein')
|
|
994
|
+
@click.option('--output-dir', '-o', type=click.Path(), help='Output directory for results')
|
|
995
|
+
@click.option('--no-affinity', is_flag=True, help='Disable affinity prediction')
|
|
996
|
+
@click.option('--pocket-residues', type=str, help='Comma-separated list of pocket residue indices')
|
|
997
|
+
@click.option('--pocket-radius', type=float, default=10.0, help='Pocket constraint radius in Angstroms')
|
|
998
|
+
@click.option('--recycling-steps', type=int, default=2, help='Number of recycling steps')
|
|
999
|
+
@click.option('--sampling-steps', type=int, default=30, help='Number of sampling steps')
|
|
1000
|
+
@click.option('--max-workers', type=int, default=4, help='Maximum parallel workers')
|
|
1001
|
+
@click.option('--batch-size', type=int, help='Process compounds in batches')
|
|
1002
|
+
@click.option('--save-structures/--no-save-structures', default=True, help='Save structure files')
|
|
1003
|
+
@click.pass_context
|
|
1004
|
+
def screen(ctx, target_sequence, compounds_file, target_name, output_dir, no_affinity,
|
|
1005
|
+
pocket_residues, pocket_radius, recycling_steps, sampling_steps,
|
|
1006
|
+
max_workers, batch_size, save_structures):
|
|
1007
|
+
"""Run virtual screening campaign against a protein target.
|
|
1008
|
+
|
|
1009
|
+
Examples:
|
|
1010
|
+
boltz2 screen "MKTVRQERLK..." compounds.csv -o results/
|
|
1011
|
+
boltz2 screen target.fasta library.json --pocket-residues "10,15,20,25"
|
|
1012
|
+
"""
|
|
1013
|
+
console = ctx.obj["console"]
|
|
1014
|
+
client = ctx.obj["client"]
|
|
1015
|
+
|
|
1016
|
+
# Import here to avoid circular imports
|
|
1017
|
+
from .virtual_screening import VirtualScreening, CompoundLibrary
|
|
1018
|
+
|
|
1019
|
+
console.print(f"\n[bold cyan]🧬 Virtual Screening Campaign[/bold cyan]")
|
|
1020
|
+
console.print(f"Target: {target_name}")
|
|
1021
|
+
console.print(f"Compounds: {compounds_file}")
|
|
1022
|
+
|
|
1023
|
+
# Load target sequence if file
|
|
1024
|
+
if Path(target_sequence).exists():
|
|
1025
|
+
with open(target_sequence, 'r') as f:
|
|
1026
|
+
lines = f.readlines()
|
|
1027
|
+
target_sequence = ''.join(line.strip() for line in lines if not line.startswith('>'))
|
|
1028
|
+
|
|
1029
|
+
console.print(f"Target length: {len(target_sequence)} residues")
|
|
1030
|
+
|
|
1031
|
+
# Parse pocket residues
|
|
1032
|
+
pocket_residues_list = None
|
|
1033
|
+
if pocket_residues:
|
|
1034
|
+
pocket_residues_list = [int(x.strip()) for x in pocket_residues.split(',')]
|
|
1035
|
+
console.print(f"Pocket constraint: {len(pocket_residues_list)} residues, radius={pocket_radius}Å")
|
|
1036
|
+
|
|
1037
|
+
# Create screener
|
|
1038
|
+
screener = VirtualScreening(client=client, max_workers=max_workers)
|
|
1039
|
+
|
|
1040
|
+
# Progress callback
|
|
1041
|
+
with Progress(
|
|
1042
|
+
SpinnerColumn(),
|
|
1043
|
+
TextColumn("[progress.description]{task.description}"),
|
|
1044
|
+
BarColumn(),
|
|
1045
|
+
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
1046
|
+
TimeElapsedColumn(),
|
|
1047
|
+
console=console
|
|
1048
|
+
) as progress:
|
|
1049
|
+
|
|
1050
|
+
task_id = None
|
|
1051
|
+
def update_progress(completed, total):
|
|
1052
|
+
nonlocal task_id
|
|
1053
|
+
if task_id is None:
|
|
1054
|
+
task_id = progress.add_task("Screening compounds", total=total)
|
|
1055
|
+
progress.update(task_id, completed=completed)
|
|
1056
|
+
|
|
1057
|
+
try:
|
|
1058
|
+
# Run screening
|
|
1059
|
+
result = screener.screen(
|
|
1060
|
+
target_sequence=target_sequence,
|
|
1061
|
+
compound_library=compounds_file,
|
|
1062
|
+
target_name=target_name,
|
|
1063
|
+
predict_affinity=not no_affinity,
|
|
1064
|
+
pocket_residues=pocket_residues_list,
|
|
1065
|
+
pocket_radius=pocket_radius,
|
|
1066
|
+
recycling_steps=recycling_steps,
|
|
1067
|
+
sampling_steps=sampling_steps,
|
|
1068
|
+
batch_size=batch_size,
|
|
1069
|
+
progress_callback=update_progress
|
|
1070
|
+
)
|
|
1071
|
+
|
|
1072
|
+
# Display results
|
|
1073
|
+
console.print(f"\n[bold green]✅ Screening completed![/bold green]")
|
|
1074
|
+
console.print(f"Total compounds: {len(result.results)}")
|
|
1075
|
+
console.print(f"Successful: {len(result.successful_results)} ({result.success_rate:.1%})")
|
|
1076
|
+
console.print(f"Duration: {result.duration_seconds:.1f} seconds")
|
|
1077
|
+
|
|
1078
|
+
# Show top hits
|
|
1079
|
+
if result.successful_results and not no_affinity:
|
|
1080
|
+
top_hits = result.get_top_hits(n=5)
|
|
1081
|
+
if not top_hits.empty:
|
|
1082
|
+
console.print("\n[bold]Top 5 Hits by pIC50:[/bold]")
|
|
1083
|
+
table = Table(show_header=True, header_style="bold magenta")
|
|
1084
|
+
table.add_column("Compound", style="cyan")
|
|
1085
|
+
table.add_column("pIC50", justify="right")
|
|
1086
|
+
table.add_column("IC50 (nM)", justify="right")
|
|
1087
|
+
table.add_column("Binding Prob", justify="right")
|
|
1088
|
+
|
|
1089
|
+
for _, hit in top_hits.iterrows():
|
|
1090
|
+
table.add_row(
|
|
1091
|
+
hit['compound_name'],
|
|
1092
|
+
f"{hit['predicted_pic50']:.2f}",
|
|
1093
|
+
f"{hit['predicted_ic50_nm']:.1f}",
|
|
1094
|
+
f"{hit['binding_probability']:.1%}"
|
|
1095
|
+
)
|
|
1096
|
+
|
|
1097
|
+
console.print(table)
|
|
1098
|
+
|
|
1099
|
+
# Save results
|
|
1100
|
+
if output_dir:
|
|
1101
|
+
saved = result.save_results(output_dir, save_structures=save_structures)
|
|
1102
|
+
console.print(f"\n[bold]Results saved to {output_dir}:[/bold]")
|
|
1103
|
+
for key, path in saved.items():
|
|
1104
|
+
console.print(f" - {key}: {path}")
|
|
1105
|
+
|
|
1106
|
+
except Exception as e:
|
|
1107
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
1108
|
+
raise click.Abort()
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
@cli.command()
|
|
1112
|
+
@click.pass_context
|
|
1113
|
+
def examples(ctx):
|
|
1114
|
+
"""Show example configurations and usage patterns."""
|
|
1115
|
+
console.print("\n[bold cyan]Boltz-2 Python Client Examples[/bold cyan]\n")
|
|
1116
|
+
|
|
1117
|
+
# Basic protein folding
|
|
1118
|
+
console.print("[bold]1. Basic Protein Folding[/bold]")
|
|
1119
|
+
console.print("boltz2 protein \"MKTVRQERLKSIVRILERSKEPVSGAQLAEELSVSRQVIVQDIAYLRSLGYNIVATPRGYVLAGG\"")
|
|
1120
|
+
console.print()
|
|
1121
|
+
|
|
1122
|
+
# Protein-ligand complex
|
|
1123
|
+
console.print("[bold]2. Protein-Ligand Complex[/bold]")
|
|
1124
|
+
console.print("boltz2 ligand \"PROTEIN_SEQUENCE\" --smiles \"CC(=O)OC1=CC=CC=C1C(=O)O\"")
|
|
1125
|
+
console.print()
|
|
1126
|
+
|
|
1127
|
+
# Covalent complex
|
|
1128
|
+
console.print("[bold]3. Covalent Complex[/bold]")
|
|
1129
|
+
console.print("boltz2 covalent \"PROTEIN_SEQUENCE\" --ccd U4U --bond A:12:SG:LIG:C22")
|
|
1130
|
+
console.print()
|
|
1131
|
+
|
|
1132
|
+
# DNA-protein complex
|
|
1133
|
+
console.print("[bold]4. DNA-Protein Complex[/bold]")
|
|
1134
|
+
console.print("boltz2 dna-protein --protein-sequences \"SEQ1,SEQ2\" --dna-sequences \"ATCG,GCTA\"")
|
|
1135
|
+
console.print()
|
|
1136
|
+
|
|
1137
|
+
# YAML configuration examples
|
|
1138
|
+
console.print("[bold]5. YAML Configuration Examples[/bold]")
|
|
1139
|
+
|
|
1140
|
+
# Basic YAML
|
|
1141
|
+
console.print("\n[bold yellow]Basic Protein-Ligand YAML:[/bold yellow]")
|
|
1142
|
+
yaml_example = """version: 1
|
|
1143
|
+
sequences:
|
|
1144
|
+
- protein:
|
|
1145
|
+
id: A
|
|
1146
|
+
sequence: "MKTVRQERLKSIVRILERSKEPVSGAQLAEELSVSRQVIVQDIAYLRSLGYNIVATPRGYVLAGG"
|
|
1147
|
+
- ligand:
|
|
1148
|
+
id: B
|
|
1149
|
+
smiles: "CC(=O)O"
|
|
1150
|
+
"""
|
|
1151
|
+
console.print(f"[dim]{yaml_example}[/dim]")
|
|
1152
|
+
|
|
1153
|
+
# Affinity prediction YAML
|
|
1154
|
+
console.print("[bold yellow]Affinity Prediction YAML:[/bold yellow]")
|
|
1155
|
+
affinity_example = """version: 1
|
|
1156
|
+
sequences:
|
|
1157
|
+
- protein:
|
|
1158
|
+
id: A
|
|
1159
|
+
sequence: "MKTVRQERLKSIVRILERSKEPVSGAQLAEELSVSRQVIVQDIAYLRSLGYNIVATPRGYVLAGG"
|
|
1160
|
+
msa: "protein_A.a3m" # optional MSA file
|
|
1161
|
+
- ligand:
|
|
1162
|
+
id: B
|
|
1163
|
+
smiles: "N[C@@H](Cc1ccc(O)cc1)C(=O)O"
|
|
1164
|
+
properties:
|
|
1165
|
+
affinity:
|
|
1166
|
+
binder: B
|
|
1167
|
+
"""
|
|
1168
|
+
console.print(f"[dim]{affinity_example}[/dim]")
|
|
1169
|
+
|
|
1170
|
+
# YAML usage
|
|
1171
|
+
console.print("[bold]YAML Usage:[/bold]")
|
|
1172
|
+
console.print("boltz2 yaml protein_ligand.yaml")
|
|
1173
|
+
console.print("boltz2 yaml my_affinity_config.yaml --recycling-steps 5 --diffusion-samples 3")
|
|
1174
|
+
console.print()
|
|
1175
|
+
|
|
1176
|
+
# Advanced JSON config
|
|
1177
|
+
console.print("[bold]6. Advanced JSON Configuration[/bold]")
|
|
1178
|
+
json_example = """{
|
|
1179
|
+
"polymers": [
|
|
1180
|
+
{
|
|
1181
|
+
"id": "A",
|
|
1182
|
+
"molecule_type": "protein",
|
|
1183
|
+
"sequence": "MKTVRQERLK..."
|
|
1184
|
+
}
|
|
1185
|
+
],
|
|
1186
|
+
"ligands": [
|
|
1187
|
+
{
|
|
1188
|
+
"id": "LIG",
|
|
1189
|
+
"smiles": "CC(=O)O"
|
|
1190
|
+
}
|
|
1191
|
+
],
|
|
1192
|
+
"recycling_steps": 5,
|
|
1193
|
+
"sampling_steps": 100,
|
|
1194
|
+
"diffusion_samples": 3,
|
|
1195
|
+
"step_scale": 2.0
|
|
1196
|
+
}"""
|
|
1197
|
+
console.print(f"[dim]{json_example}[/dim]")
|
|
1198
|
+
console.print("boltz2 advanced --config-file advanced_config.json")
|
|
1199
|
+
console.print()
|
|
1200
|
+
|
|
1201
|
+
# Endpoint configuration
|
|
1202
|
+
console.print("[bold]7. Endpoint Configuration[/bold]")
|
|
1203
|
+
console.print("# Local endpoint (default)")
|
|
1204
|
+
console.print("boltz2 --base-url http://localhost:8000 protein \"SEQUENCE\"")
|
|
1205
|
+
console.print()
|
|
1206
|
+
console.print("# NVIDIA hosted endpoint")
|
|
1207
|
+
console.print("export NVIDIA_API_KEY=your_api_key")
|
|
1208
|
+
console.print("boltz2 --base-url https://health.api.nvidia.com --endpoint-type nvidia_hosted protein \"SEQUENCE\"")
|
|
1209
|
+
console.print()
|
|
1210
|
+
|
|
1211
|
+
|
|
1212
|
+
if __name__ == "__main__":
|
|
1213
|
+
cli()
|