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/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()