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.
@@ -0,0 +1,46 @@
1
+ # ---------------------------------------------------------------
2
+ # Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
3
+ # ---------------------------------------------------------------
4
+
5
+ """
6
+ Affinity prediction models for Boltz-2 API.
7
+
8
+ This module defines the affinity-related models for the new affinity
9
+ prediction capabilities in Boltz-2.
10
+ """
11
+
12
+ from typing import List, Optional, Dict, Any
13
+ from pydantic import BaseModel, Field
14
+
15
+
16
+ class AffinityPrediction(BaseModel):
17
+ """Affinity prediction results for a ligand."""
18
+
19
+ affinity_pred_value: List[float] = Field(
20
+ ...,
21
+ description="The predicted log(IC50) values"
22
+ )
23
+ affinity_probability_binary: List[float] = Field(
24
+ ...,
25
+ description="The binary affinity prediction probability (0-1)"
26
+ )
27
+ model_1_affinity_pred_value: List[float] = Field(
28
+ ...,
29
+ description="The predicted log(IC50) from Model 1"
30
+ )
31
+ model_1_affinity_probability_binary: List[float] = Field(
32
+ ...,
33
+ description="The binary affinity prediction probability from Model 1"
34
+ )
35
+ model_2_affinity_pred_value: List[float] = Field(
36
+ ...,
37
+ description="The predicted log(IC50) from Model 2"
38
+ )
39
+ model_2_affinity_probability_binary: List[float] = Field(
40
+ ...,
41
+ description="The binary affinity prediction probability from Model 2"
42
+ )
43
+ affinity_pic50: List[float] = Field(
44
+ ...,
45
+ description="Predicted pIC50 binding affinity (kcal/mol)"
46
+ )
boltz2_client/utils.py ADDED
@@ -0,0 +1,455 @@
1
+ # ---------------------------------------------------------------
2
+ # Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
3
+ # ---------------------------------------------------------------
4
+
5
+
6
+ """
7
+ Utility functions for the Boltz-2 Python client.
8
+
9
+ This module provides helper functions for sequence validation, file I/O,
10
+ structure manipulation, and other common tasks.
11
+ """
12
+
13
+ import re
14
+ import json
15
+ from pathlib import Path
16
+ from typing import Dict, Any, Optional, Union, List
17
+ from datetime import datetime
18
+
19
+ import aiofiles
20
+
21
+
22
+ def validate_sequence(sequence: str, molecule_type: str) -> bool:
23
+ """
24
+ Validate a molecular sequence.
25
+
26
+ Args:
27
+ sequence: The sequence string to validate
28
+ molecule_type: Type of molecule ('protein', 'dna', 'rna')
29
+
30
+ Returns:
31
+ True if sequence is valid
32
+
33
+ Raises:
34
+ ValueError: If sequence is invalid
35
+ """
36
+ if not sequence or not sequence.strip():
37
+ raise ValueError("Sequence cannot be empty")
38
+
39
+ sequence = sequence.upper().strip()
40
+
41
+ if molecule_type.lower() == "protein":
42
+ # Standard amino acid codes
43
+ valid_chars = set("ACDEFGHIKLMNPQRSTVWY")
44
+ invalid_chars = set(sequence) - valid_chars
45
+ if invalid_chars:
46
+ raise ValueError(f"Invalid amino acid characters: {invalid_chars}")
47
+
48
+ elif molecule_type.lower() == "dna":
49
+ # Standard DNA bases
50
+ valid_chars = set("ATCG")
51
+ invalid_chars = set(sequence) - valid_chars
52
+ if invalid_chars:
53
+ raise ValueError(f"Invalid DNA base characters: {invalid_chars}")
54
+
55
+ elif molecule_type.lower() == "rna":
56
+ # Standard RNA bases
57
+ valid_chars = set("AUCG")
58
+ invalid_chars = set(sequence) - valid_chars
59
+ if invalid_chars:
60
+ raise ValueError(f"Invalid RNA base characters: {invalid_chars}")
61
+
62
+ else:
63
+ raise ValueError(f"Unknown molecule type: {molecule_type}")
64
+
65
+ return True
66
+
67
+
68
+ def parse_mmcif(mmcif_data: str) -> Dict[str, Any]:
69
+ """
70
+ Parse mmCIF data and extract basic information.
71
+
72
+ Args:
73
+ mmcif_data: mmCIF format string
74
+
75
+ Returns:
76
+ Dictionary with parsed information
77
+ """
78
+ info = {
79
+ "atoms": [],
80
+ "chains": set(),
81
+ "residues": set(),
82
+ "metadata": {}
83
+ }
84
+
85
+ lines = mmcif_data.strip().split('\n')
86
+
87
+ for line in lines:
88
+ line = line.strip()
89
+
90
+ # Parse atom records
91
+ if line.startswith('ATOM') or line.startswith('HETATM'):
92
+ parts = line.split()
93
+ if len(parts) >= 11:
94
+ atom_info = {
95
+ "type": parts[0],
96
+ "id": parts[1],
97
+ "atom_name": parts[2],
98
+ "residue_name": parts[3],
99
+ "chain": parts[4],
100
+ "residue_id": parts[5],
101
+ "x": float(parts[6]),
102
+ "y": float(parts[7]),
103
+ "z": float(parts[8]),
104
+ "occupancy": float(parts[9]) if parts[9] != '?' else 1.0,
105
+ "b_factor": float(parts[10]) if parts[10] != '?' else 0.0,
106
+ }
107
+ info["atoms"].append(atom_info)
108
+ info["chains"].add(atom_info["chain"])
109
+ info["residues"].add(f"{atom_info['chain']}:{atom_info['residue_name']}{atom_info['residue_id']}")
110
+
111
+ # Convert sets to lists for JSON serialization
112
+ info["chains"] = list(info["chains"])
113
+ info["residues"] = list(info["residues"])
114
+
115
+ return info
116
+
117
+
118
+ def save_structure(structure_data: str, filepath: Union[str, Path]) -> Path:
119
+ """
120
+ Save structure data to a file.
121
+
122
+ Args:
123
+ structure_data: Structure data (mmCIF format)
124
+ filepath: Output file path
125
+
126
+ Returns:
127
+ Path object of saved file
128
+ """
129
+ filepath = Path(filepath)
130
+ filepath.parent.mkdir(parents=True, exist_ok=True)
131
+
132
+ with open(filepath, 'w') as f:
133
+ f.write(structure_data)
134
+
135
+ return filepath
136
+
137
+
138
+ async def save_structure_async(structure_data: str, filepath: Union[str, Path]) -> Path:
139
+ """
140
+ Asynchronously save structure data to a file.
141
+
142
+ Args:
143
+ structure_data: Structure data (mmCIF format)
144
+ filepath: Output file path
145
+
146
+ Returns:
147
+ Path object of saved file
148
+ """
149
+ filepath = Path(filepath)
150
+ filepath.parent.mkdir(parents=True, exist_ok=True)
151
+
152
+ async with aiofiles.open(filepath, 'w') as f:
153
+ await f.write(structure_data)
154
+
155
+ return filepath
156
+
157
+
158
+ def load_structure(filepath: Union[str, Path]) -> str:
159
+ """
160
+ Load structure data from a file.
161
+
162
+ Args:
163
+ filepath: Input file path
164
+
165
+ Returns:
166
+ Structure data as string
167
+ """
168
+ filepath = Path(filepath)
169
+
170
+ if not filepath.exists():
171
+ raise FileNotFoundError(f"Structure file not found: {filepath}")
172
+
173
+ with open(filepath, 'r') as f:
174
+ return f.read()
175
+
176
+
177
+ async def load_structure_async(filepath: Union[str, Path]) -> str:
178
+ """
179
+ Asynchronously load structure data from a file.
180
+
181
+ Args:
182
+ filepath: Input file path
183
+
184
+ Returns:
185
+ Structure data as string
186
+ """
187
+ filepath = Path(filepath)
188
+
189
+ if not filepath.exists():
190
+ raise FileNotFoundError(f"Structure file not found: {filepath}")
191
+
192
+ async with aiofiles.open(filepath, 'r') as f:
193
+ return await f.read()
194
+
195
+
196
+ def save_json(data: Dict[str, Any], filepath: Union[str, Path]) -> Path:
197
+ """
198
+ Save data as JSON file.
199
+
200
+ Args:
201
+ data: Data to save
202
+ filepath: Output file path
203
+
204
+ Returns:
205
+ Path object of saved file
206
+ """
207
+ filepath = Path(filepath)
208
+ filepath.parent.mkdir(parents=True, exist_ok=True)
209
+
210
+ with open(filepath, 'w') as f:
211
+ json.dump(data, f, indent=2, default=str)
212
+
213
+ return filepath
214
+
215
+
216
+ def load_json(filepath: Union[str, Path]) -> Dict[str, Any]:
217
+ """
218
+ Load data from JSON file.
219
+
220
+ Args:
221
+ filepath: Input file path
222
+
223
+ Returns:
224
+ Loaded data
225
+ """
226
+ filepath = Path(filepath)
227
+
228
+ if not filepath.exists():
229
+ raise FileNotFoundError(f"JSON file not found: {filepath}")
230
+
231
+ with open(filepath, 'r') as f:
232
+ return json.load(f)
233
+
234
+
235
+ def format_sequence(sequence: str, line_length: int = 80) -> str:
236
+ """
237
+ Format a sequence with line breaks.
238
+
239
+ Args:
240
+ sequence: Input sequence
241
+ line_length: Maximum line length
242
+
243
+ Returns:
244
+ Formatted sequence string
245
+ """
246
+ sequence = sequence.strip()
247
+ lines = []
248
+
249
+ for i in range(0, len(sequence), line_length):
250
+ lines.append(sequence[i:i + line_length])
251
+
252
+ return '\n'.join(lines)
253
+
254
+
255
+ def calculate_sequence_stats(sequence: str, molecule_type: str) -> Dict[str, Any]:
256
+ """
257
+ Calculate basic statistics for a sequence.
258
+
259
+ Args:
260
+ sequence: Input sequence
261
+ molecule_type: Type of molecule ('protein', 'dna', 'rna')
262
+
263
+ Returns:
264
+ Dictionary with sequence statistics
265
+ """
266
+ sequence = sequence.upper().strip()
267
+ length = len(sequence)
268
+
269
+ stats = {
270
+ "length": length,
271
+ "composition": {},
272
+ "molecular_weight": 0.0,
273
+ "type": molecule_type.lower()
274
+ }
275
+
276
+ # Count composition
277
+ for char in set(sequence):
278
+ stats["composition"][char] = sequence.count(char)
279
+
280
+ # Calculate molecular weight (approximate)
281
+ if molecule_type.lower() == "protein":
282
+ # Average amino acid molecular weight
283
+ aa_weights = {
284
+ 'A': 89.1, 'R': 174.2, 'N': 132.1, 'D': 133.1, 'C': 121.2,
285
+ 'Q': 146.1, 'E': 147.1, 'G': 75.1, 'H': 155.2, 'I': 131.2,
286
+ 'L': 131.2, 'K': 146.2, 'M': 149.2, 'F': 165.2, 'P': 115.1,
287
+ 'S': 105.1, 'T': 119.1, 'W': 204.2, 'Y': 181.2, 'V': 117.1
288
+ }
289
+ stats["molecular_weight"] = sum(aa_weights.get(aa, 110.0) for aa in sequence)
290
+
291
+ elif molecule_type.lower() in ["dna", "rna"]:
292
+ # Average nucleotide molecular weight
293
+ if molecule_type.lower() == "dna":
294
+ nt_weights = {'A': 331.2, 'T': 322.2, 'C': 307.2, 'G': 347.2}
295
+ else: # RNA
296
+ nt_weights = {'A': 347.2, 'U': 324.2, 'C': 323.2, 'G': 363.2}
297
+
298
+ stats["molecular_weight"] = sum(nt_weights.get(nt, 330.0) for nt in sequence)
299
+
300
+ return stats
301
+
302
+
303
+ def generate_timestamp() -> str:
304
+ """
305
+ Generate a timestamp string for file naming.
306
+
307
+ Returns:
308
+ Timestamp string in format YYYYMMDD_HHMMSS
309
+ """
310
+ return datetime.now().strftime("%Y%m%d_%H%M%S")
311
+
312
+
313
+ def sanitize_filename(filename: str) -> str:
314
+ """
315
+ Sanitize a filename by removing invalid characters.
316
+
317
+ Args:
318
+ filename: Input filename
319
+
320
+ Returns:
321
+ Sanitized filename
322
+ """
323
+ # Remove invalid characters
324
+ filename = re.sub(r'[<>:"/\\|?*]', '_', filename)
325
+
326
+ # Remove leading/trailing spaces and dots
327
+ filename = filename.strip(' .')
328
+
329
+ # Ensure it's not empty
330
+ if not filename:
331
+ filename = "unnamed"
332
+
333
+ return filename
334
+
335
+
336
+ def create_output_directory(base_dir: Union[str, Path], prefix: str = "boltz2_output") -> Path:
337
+ """
338
+ Create a timestamped output directory.
339
+
340
+ Args:
341
+ base_dir: Base directory path
342
+ prefix: Directory name prefix
343
+
344
+ Returns:
345
+ Created directory path
346
+ """
347
+ base_dir = Path(base_dir)
348
+ timestamp = generate_timestamp()
349
+ output_dir = base_dir / f"{prefix}_{timestamp}"
350
+ output_dir.mkdir(parents=True, exist_ok=True)
351
+
352
+ return output_dir
353
+
354
+
355
+ def validate_smiles(smiles: str) -> bool:
356
+ """
357
+ Basic SMILES string validation.
358
+
359
+ Args:
360
+ smiles: SMILES string to validate
361
+
362
+ Returns:
363
+ True if SMILES appears valid
364
+
365
+ Raises:
366
+ ValueError: If SMILES is invalid
367
+ """
368
+ if not smiles or not smiles.strip():
369
+ raise ValueError("SMILES string cannot be empty")
370
+
371
+ smiles = smiles.strip()
372
+
373
+ # Basic character validation
374
+ if any(char in smiles for char in [' ', '\t', '\n']):
375
+ raise ValueError("SMILES string should not contain whitespace")
376
+
377
+ # Check for balanced parentheses
378
+ paren_count = 0
379
+ bracket_count = 0
380
+
381
+ for char in smiles:
382
+ if char == '(':
383
+ paren_count += 1
384
+ elif char == ')':
385
+ paren_count -= 1
386
+ elif char == '[':
387
+ bracket_count += 1
388
+ elif char == ']':
389
+ bracket_count -= 1
390
+
391
+ if paren_count < 0 or bracket_count < 0:
392
+ raise ValueError("Unbalanced parentheses or brackets in SMILES")
393
+
394
+ if paren_count != 0:
395
+ raise ValueError("Unbalanced parentheses in SMILES")
396
+
397
+ if bracket_count != 0:
398
+ raise ValueError("Unbalanced brackets in SMILES")
399
+
400
+ return True
401
+
402
+
403
+ def extract_chains_from_mmcif(mmcif_data: str) -> List[str]:
404
+ """
405
+ Extract chain IDs from mmCIF data.
406
+
407
+ Args:
408
+ mmcif_data: mmCIF format string
409
+
410
+ Returns:
411
+ List of unique chain IDs
412
+ """
413
+ chains = set()
414
+ lines = mmcif_data.strip().split('\n')
415
+
416
+ for line in lines:
417
+ line = line.strip()
418
+ if line.startswith('ATOM') or line.startswith('HETATM'):
419
+ parts = line.split()
420
+ if len(parts) >= 5:
421
+ chain_id = parts[4]
422
+ chains.add(chain_id)
423
+
424
+ return sorted(list(chains))
425
+
426
+
427
+ def get_structure_summary(mmcif_data: str) -> Dict[str, Any]:
428
+ """
429
+ Get a summary of structure information.
430
+
431
+ Args:
432
+ mmcif_data: mmCIF format string
433
+
434
+ Returns:
435
+ Dictionary with structure summary
436
+ """
437
+ info = parse_mmcif(mmcif_data)
438
+
439
+ summary = {
440
+ "total_atoms": len(info["atoms"]),
441
+ "chains": len(info["chains"]),
442
+ "residues": len(info["residues"]),
443
+ "chain_list": info["chains"],
444
+ "size_estimate_mb": len(mmcif_data) / (1024 * 1024),
445
+ }
446
+
447
+ # Count atoms by type
448
+ atom_types = {}
449
+ for atom in info["atoms"]:
450
+ atom_type = atom["type"]
451
+ atom_types[atom_type] = atom_types.get(atom_type, 0) + 1
452
+
453
+ summary["atom_types"] = atom_types
454
+
455
+ return summary