boltz2-python-client 0.3.2__py3-none-any.whl → 0.4.0__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 +15 -1
- boltz2_client/a3m_to_csv_converter.py +363 -12
- boltz2_client/cli.py +150 -23
- boltz2_client/client.py +101 -6
- boltz2_client/models.py +92 -29
- boltz2_client/multi_endpoint_client.py +1 -1
- {boltz2_python_client-0.3.2.dist-info → boltz2_python_client-0.4.0.dist-info}/METADATA +89 -15
- boltz2_python_client-0.4.0.dist-info/RECORD +20 -0
- {boltz2_python_client-0.3.2.dist-info → boltz2_python_client-0.4.0.dist-info}/WHEEL +1 -1
- boltz2_python_client-0.3.2.dist-info/RECORD +0 -20
- {boltz2_python_client-0.3.2.dist-info → boltz2_python_client-0.4.0.dist-info}/entry_points.txt +0 -0
- {boltz2_python_client-0.3.2.dist-info → boltz2_python_client-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {boltz2_python_client-0.3.2.dist-info → boltz2_python_client-0.4.0.dist-info}/top_level.txt +0 -0
boltz2_client/__init__.py
CHANGED
|
@@ -26,7 +26,7 @@ Example:
|
|
|
26
26
|
>>> print(f"Confidence: {result.confidence_scores[0]:.3f}")
|
|
27
27
|
"""
|
|
28
28
|
|
|
29
|
-
__version__ = "0.
|
|
29
|
+
__version__ = "0.4.0"
|
|
30
30
|
__author__ = "NVIDIA Corporation"
|
|
31
31
|
__email__ = "bionemo-support@nvidia.com"
|
|
32
32
|
|
|
@@ -43,6 +43,8 @@ from .models import (
|
|
|
43
43
|
AlignmentFormat,
|
|
44
44
|
HealthStatus,
|
|
45
45
|
ServiceMetadata,
|
|
46
|
+
StructuralTemplate,
|
|
47
|
+
Modification,
|
|
46
48
|
)
|
|
47
49
|
from .models_affinity import AffinityPrediction
|
|
48
50
|
from .exceptions import (
|
|
@@ -81,6 +83,11 @@ from .a3m_to_csv_converter import (
|
|
|
81
83
|
create_paired_msa_per_chain,
|
|
82
84
|
save_prediction_outputs,
|
|
83
85
|
get_prediction_summary,
|
|
86
|
+
save_pae_matrix,
|
|
87
|
+
save_pde_matrix,
|
|
88
|
+
get_pae_summary,
|
|
89
|
+
convert_cif_to_pdb,
|
|
90
|
+
convert_pdb_to_cif,
|
|
84
91
|
ConversionResult,
|
|
85
92
|
GreedyPairingStrategy,
|
|
86
93
|
CompletePairingStrategy,
|
|
@@ -130,6 +137,8 @@ __all__ = [
|
|
|
130
137
|
"HealthStatus",
|
|
131
138
|
"ServiceMetadata",
|
|
132
139
|
"AffinityPrediction",
|
|
140
|
+
"StructuralTemplate",
|
|
141
|
+
"Modification",
|
|
133
142
|
|
|
134
143
|
# Exceptions
|
|
135
144
|
"Boltz2Error",
|
|
@@ -167,6 +176,11 @@ __all__ = [
|
|
|
167
176
|
"create_paired_msa_per_chain",
|
|
168
177
|
"save_prediction_outputs",
|
|
169
178
|
"get_prediction_summary",
|
|
179
|
+
"save_pae_matrix",
|
|
180
|
+
"save_pde_matrix",
|
|
181
|
+
"get_pae_summary",
|
|
182
|
+
"convert_cif_to_pdb",
|
|
183
|
+
"convert_pdb_to_cif",
|
|
170
184
|
"ConversionResult",
|
|
171
185
|
"GreedyPairingStrategy",
|
|
172
186
|
"CompletePairingStrategy",
|
|
@@ -23,7 +23,7 @@ Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
|
|
|
23
23
|
import re
|
|
24
24
|
import csv
|
|
25
25
|
import logging
|
|
26
|
-
from typing import Dict, List, Optional, Tuple, Set, NamedTuple
|
|
26
|
+
from typing import Dict, List, Optional, Tuple, Set, NamedTuple, Union
|
|
27
27
|
from pathlib import Path
|
|
28
28
|
from dataclasses import dataclass, field
|
|
29
29
|
from collections import defaultdict
|
|
@@ -1192,6 +1192,11 @@ class A3MToCSVConverter:
|
|
|
1192
1192
|
- Rows with the same 'key' across different chain CSVs are paired
|
|
1193
1193
|
- This enables the model to identify co-evolved sequences from same organisms
|
|
1194
1194
|
|
|
1195
|
+
When include_unpaired=True, also includes:
|
|
1196
|
+
- Unpaired sequences in "block-diagonal" format where only one chain has
|
|
1197
|
+
a sequence and others have gaps. This maximizes MSA depth while still
|
|
1198
|
+
providing pairing information where available.
|
|
1199
|
+
|
|
1195
1200
|
Args:
|
|
1196
1201
|
msas: Dictionary mapping chain IDs to parsed A3MMSA objects
|
|
1197
1202
|
output_path: Optional path to save the combined CSV file
|
|
@@ -1204,35 +1209,79 @@ class A3MToCSVConverter:
|
|
|
1204
1209
|
# Find paired sequences
|
|
1205
1210
|
pairs = self.pairing_strategy.find_pairs(msas)
|
|
1206
1211
|
|
|
1212
|
+
# Track which sequences have been paired (to find unpaired ones later)
|
|
1213
|
+
paired_sequence_ids: Dict[str, Set[str]] = {chain_id: set() for chain_id in chain_ids}
|
|
1214
|
+
for pair in pairs:
|
|
1215
|
+
for chain_id, seq in pair.items():
|
|
1216
|
+
paired_sequence_ids[chain_id].add(seq.identifier)
|
|
1217
|
+
|
|
1207
1218
|
if self.max_pairs:
|
|
1208
1219
|
pairs = pairs[:self.max_pairs]
|
|
1209
1220
|
|
|
1210
1221
|
logger.info(f"Found {len(pairs)} paired sequence sets across {len(chain_ids)} chains")
|
|
1211
1222
|
|
|
1212
|
-
# Extract query sequences
|
|
1223
|
+
# Extract query sequences and their lengths (for gap filling)
|
|
1213
1224
|
query_sequences = {}
|
|
1225
|
+
query_lengths = {}
|
|
1214
1226
|
for chain_id, msa in msas.items():
|
|
1215
1227
|
query = msa.get_query()
|
|
1216
1228
|
if query:
|
|
1217
|
-
|
|
1229
|
+
clean_seq = self._clean_sequence(query.sequence)
|
|
1230
|
+
query_sequences[chain_id] = clean_seq
|
|
1231
|
+
query_lengths[chain_id] = len(clean_seq)
|
|
1232
|
+
|
|
1233
|
+
# Collect unpaired sequences if requested
|
|
1234
|
+
unpaired_sequences: Dict[str, List[A3MSequence]] = {chain_id: [] for chain_id in chain_ids}
|
|
1235
|
+
num_unpaired = 0
|
|
1236
|
+
|
|
1237
|
+
if self.include_unpaired:
|
|
1238
|
+
for chain_id, msa in msas.items():
|
|
1239
|
+
for seq in msa.sequences:
|
|
1240
|
+
if seq.is_query:
|
|
1241
|
+
continue
|
|
1242
|
+
if seq.identifier not in paired_sequence_ids[chain_id]:
|
|
1243
|
+
unpaired_sequences[chain_id].append(seq)
|
|
1244
|
+
num_unpaired += 1
|
|
1245
|
+
|
|
1246
|
+
logger.info(f"Found {num_unpaired} unpaired sequences to include in block-diagonal format")
|
|
1247
|
+
|
|
1248
|
+
# Calculate total rows: paired + unpaired (block-diagonal)
|
|
1249
|
+
current_key = len(pairs) # Start unpaired keys after paired ones
|
|
1218
1250
|
|
|
1219
1251
|
# Build per-chain CSVs (for Boltz2 NIM API)
|
|
1220
1252
|
csv_per_chain: Dict[str, str] = {}
|
|
1221
1253
|
for chain_id in chain_ids:
|
|
1222
1254
|
chain_lines = ["key,sequence"]
|
|
1255
|
+
|
|
1256
|
+
# Add paired sequences
|
|
1223
1257
|
for idx, pair in enumerate(pairs, start=1):
|
|
1224
1258
|
if chain_id in pair:
|
|
1225
1259
|
seq = self._clean_sequence(pair[chain_id].sequence)
|
|
1226
1260
|
chain_lines.append(f"{idx},{seq}")
|
|
1227
1261
|
else:
|
|
1228
1262
|
# Use gaps if sequence not found for this chain
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
gap_seq = '-' * len(self._clean_sequence(query.sequence))
|
|
1233
|
-
chain_lines.append(f"{idx},{gap_seq}")
|
|
1263
|
+
gap_seq = '-' * query_lengths.get(chain_id, 0)
|
|
1264
|
+
chain_lines.append(f"{idx},{gap_seq}")
|
|
1265
|
+
|
|
1234
1266
|
csv_per_chain[chain_id] = '\n'.join(chain_lines)
|
|
1235
1267
|
|
|
1268
|
+
# Add unpaired sequences in block-diagonal format
|
|
1269
|
+
if self.include_unpaired:
|
|
1270
|
+
unpaired_key = current_key + 1
|
|
1271
|
+
for source_chain_id in chain_ids:
|
|
1272
|
+
for unpaired_seq in unpaired_sequences[source_chain_id]:
|
|
1273
|
+
# Add this unpaired sequence to all chain CSVs
|
|
1274
|
+
for chain_id in chain_ids:
|
|
1275
|
+
if chain_id == source_chain_id:
|
|
1276
|
+
# This chain has the sequence
|
|
1277
|
+
seq = self._clean_sequence(unpaired_seq.sequence)
|
|
1278
|
+
csv_per_chain[chain_id] += f"\n{unpaired_key},{seq}"
|
|
1279
|
+
else:
|
|
1280
|
+
# Other chains get gaps
|
|
1281
|
+
gap_seq = '-' * query_lengths.get(chain_id, 0)
|
|
1282
|
+
csv_per_chain[chain_id] += f"\n{unpaired_key},{gap_seq}"
|
|
1283
|
+
unpaired_key += 1
|
|
1284
|
+
|
|
1236
1285
|
# Build combined CSV (for reference/documentation)
|
|
1237
1286
|
csv_lines = ["key,sequence"]
|
|
1238
1287
|
for idx, pair in enumerate(pairs, start=1):
|
|
@@ -1242,15 +1291,31 @@ class A3MToCSVConverter:
|
|
|
1242
1291
|
seq = self._clean_sequence(pair[chain_id].sequence)
|
|
1243
1292
|
sequences.append(seq)
|
|
1244
1293
|
else:
|
|
1245
|
-
|
|
1246
|
-
query = ref_msa.get_query()
|
|
1247
|
-
if query:
|
|
1248
|
-
sequences.append('-' * len(self._clean_sequence(query.sequence)))
|
|
1294
|
+
sequences.append('-' * query_lengths.get(chain_id, 0))
|
|
1249
1295
|
concatenated = ':'.join(sequences)
|
|
1250
1296
|
csv_lines.append(f"{idx},{concatenated}")
|
|
1251
1297
|
|
|
1298
|
+
# Add unpaired sequences to combined CSV in block-diagonal format
|
|
1299
|
+
if self.include_unpaired:
|
|
1300
|
+
unpaired_key = current_key + 1
|
|
1301
|
+
for source_chain_id in chain_ids:
|
|
1302
|
+
for unpaired_seq in unpaired_sequences[source_chain_id]:
|
|
1303
|
+
sequences = []
|
|
1304
|
+
for chain_id in chain_ids:
|
|
1305
|
+
if chain_id == source_chain_id:
|
|
1306
|
+
seq = self._clean_sequence(unpaired_seq.sequence)
|
|
1307
|
+
sequences.append(seq)
|
|
1308
|
+
else:
|
|
1309
|
+
sequences.append('-' * query_lengths.get(chain_id, 0))
|
|
1310
|
+
concatenated = ':'.join(sequences)
|
|
1311
|
+
csv_lines.append(f"{unpaired_key},{concatenated}")
|
|
1312
|
+
unpaired_key += 1
|
|
1313
|
+
|
|
1252
1314
|
csv_content = '\n'.join(csv_lines)
|
|
1253
1315
|
|
|
1316
|
+
total_rows = len(pairs) + (num_unpaired if self.include_unpaired else 0)
|
|
1317
|
+
logger.info(f"Total MSA rows: {total_rows} ({len(pairs)} paired + {num_unpaired if self.include_unpaired else 0} unpaired)")
|
|
1318
|
+
|
|
1254
1319
|
# Save files if path provided
|
|
1255
1320
|
output_paths_per_chain = None
|
|
1256
1321
|
if output_path:
|
|
@@ -1355,6 +1420,7 @@ def convert_a3m_to_multimer_csv(
|
|
|
1355
1420
|
output_path: Optional[Path] = None,
|
|
1356
1421
|
pairing_strategy: str = "greedy",
|
|
1357
1422
|
use_tax_id: Optional[bool] = None, # None = auto-detect (ColabFold default)
|
|
1423
|
+
include_unpaired: bool = False,
|
|
1358
1424
|
max_pairs: Optional[int] = None
|
|
1359
1425
|
) -> ConversionResult:
|
|
1360
1426
|
"""
|
|
@@ -1374,6 +1440,10 @@ def convert_a3m_to_multimer_csv(
|
|
|
1374
1440
|
This is how ColabFold behaves.
|
|
1375
1441
|
- True: Force TaxID pairing (requires OX= or species codes in headers)
|
|
1376
1442
|
- False: Force UniRef/organism ID pairing (works with standard ColabFold output)
|
|
1443
|
+
include_unpaired: If True, include sequences without cross-chain matches in
|
|
1444
|
+
"block-diagonal" format (one chain has sequence, others have gaps).
|
|
1445
|
+
This maximizes MSA depth while still providing pairing where available.
|
|
1446
|
+
Default: False (only paired sequences are included)
|
|
1377
1447
|
max_pairs: Maximum number of pairs to include
|
|
1378
1448
|
|
|
1379
1449
|
Returns:
|
|
@@ -1388,6 +1458,12 @@ def convert_a3m_to_multimer_csv(
|
|
|
1388
1458
|
... # use_tax_id=None means auto-detect (default)
|
|
1389
1459
|
... )
|
|
1390
1460
|
>>> print(f"Created {result.num_pairs} paired sequences")
|
|
1461
|
+
|
|
1462
|
+
>>> # Include unpaired sequences for maximum MSA depth
|
|
1463
|
+
>>> result = convert_a3m_to_multimer_csv(
|
|
1464
|
+
... a3m_files={'A': Path('chain_A.a3m'), 'B': Path('chain_B.a3m')},
|
|
1465
|
+
... include_unpaired=True # Block-diagonal format
|
|
1466
|
+
... )
|
|
1391
1467
|
|
|
1392
1468
|
ColabFold Compatibility:
|
|
1393
1469
|
Standard ColabFold A3M files use UniRef100 cluster IDs without TaxID information.
|
|
@@ -1417,6 +1493,7 @@ def convert_a3m_to_multimer_csv(
|
|
|
1417
1493
|
|
|
1418
1494
|
converter = A3MToCSVConverter(
|
|
1419
1495
|
pairing_strategy=strategy,
|
|
1496
|
+
include_unpaired=include_unpaired,
|
|
1420
1497
|
max_pairs=max_pairs
|
|
1421
1498
|
)
|
|
1422
1499
|
|
|
@@ -1659,3 +1736,277 @@ def get_prediction_summary(response) -> Dict:
|
|
|
1659
1736
|
|
|
1660
1737
|
return summary
|
|
1661
1738
|
|
|
1739
|
+
|
|
1740
|
+
def save_pae_matrix(
|
|
1741
|
+
pae: List[List[List[float]]],
|
|
1742
|
+
output_path: Union[str, Path],
|
|
1743
|
+
format: str = "json"
|
|
1744
|
+
) -> Path:
|
|
1745
|
+
"""
|
|
1746
|
+
Save PAE (Predicted Aligned Error) matrix to file.
|
|
1747
|
+
|
|
1748
|
+
Args:
|
|
1749
|
+
pae: PAE matrix from PredictionResponse.pae
|
|
1750
|
+
Shape: [num_models, num_residues, num_residues]
|
|
1751
|
+
output_path: Path to save the file
|
|
1752
|
+
format: Output format - "json" or "npy" (numpy)
|
|
1753
|
+
|
|
1754
|
+
Returns:
|
|
1755
|
+
Path to saved file
|
|
1756
|
+
|
|
1757
|
+
Example:
|
|
1758
|
+
>>> if response.pae:
|
|
1759
|
+
... save_pae_matrix(response.pae, "structure.pae.json")
|
|
1760
|
+
"""
|
|
1761
|
+
import json
|
|
1762
|
+
|
|
1763
|
+
output_path = Path(output_path)
|
|
1764
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1765
|
+
|
|
1766
|
+
if format == "json":
|
|
1767
|
+
with open(output_path, "w") as f:
|
|
1768
|
+
json.dump({"pae": pae}, f, indent=2)
|
|
1769
|
+
elif format == "npy":
|
|
1770
|
+
try:
|
|
1771
|
+
import numpy as np
|
|
1772
|
+
np.save(output_path, np.array(pae))
|
|
1773
|
+
except ImportError:
|
|
1774
|
+
raise ImportError("numpy is required for .npy format. Install with: pip install numpy")
|
|
1775
|
+
else:
|
|
1776
|
+
raise ValueError(f"Unsupported format: {format}. Use 'json' or 'npy'")
|
|
1777
|
+
|
|
1778
|
+
return output_path
|
|
1779
|
+
|
|
1780
|
+
|
|
1781
|
+
def save_pde_matrix(
|
|
1782
|
+
pde: List[List[List[float]]],
|
|
1783
|
+
output_path: Union[str, Path],
|
|
1784
|
+
format: str = "json"
|
|
1785
|
+
) -> Path:
|
|
1786
|
+
"""
|
|
1787
|
+
Save PDE (Predicted Distance Error) matrix to file.
|
|
1788
|
+
|
|
1789
|
+
Args:
|
|
1790
|
+
pde: PDE matrix from PredictionResponse.pde
|
|
1791
|
+
Shape: [num_models, num_residues, num_residues]
|
|
1792
|
+
output_path: Path to save the file
|
|
1793
|
+
format: Output format - "json" or "npy" (numpy)
|
|
1794
|
+
|
|
1795
|
+
Returns:
|
|
1796
|
+
Path to saved file
|
|
1797
|
+
|
|
1798
|
+
Example:
|
|
1799
|
+
>>> if response.pde:
|
|
1800
|
+
... save_pde_matrix(response.pde, "structure.pde.json")
|
|
1801
|
+
"""
|
|
1802
|
+
import json
|
|
1803
|
+
|
|
1804
|
+
output_path = Path(output_path)
|
|
1805
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1806
|
+
|
|
1807
|
+
if format == "json":
|
|
1808
|
+
with open(output_path, "w") as f:
|
|
1809
|
+
json.dump({"pde": pde}, f, indent=2)
|
|
1810
|
+
elif format == "npy":
|
|
1811
|
+
try:
|
|
1812
|
+
import numpy as np
|
|
1813
|
+
np.save(output_path, np.array(pde))
|
|
1814
|
+
except ImportError:
|
|
1815
|
+
raise ImportError("numpy is required for .npy format. Install with: pip install numpy")
|
|
1816
|
+
else:
|
|
1817
|
+
raise ValueError(f"Unsupported format: {format}. Use 'json' or 'npy'")
|
|
1818
|
+
|
|
1819
|
+
return output_path
|
|
1820
|
+
|
|
1821
|
+
|
|
1822
|
+
def get_pae_summary(pae: List[List[List[float]]]) -> Dict:
|
|
1823
|
+
"""
|
|
1824
|
+
Get summary statistics for PAE matrix.
|
|
1825
|
+
|
|
1826
|
+
Args:
|
|
1827
|
+
pae: PAE matrix from PredictionResponse.pae
|
|
1828
|
+
|
|
1829
|
+
Returns:
|
|
1830
|
+
Dictionary with shape, mean, min, max values
|
|
1831
|
+
|
|
1832
|
+
Example:
|
|
1833
|
+
>>> summary = get_pae_summary(response.pae)
|
|
1834
|
+
>>> print(f"Mean PAE: {summary['mean']:.2f} Å")
|
|
1835
|
+
"""
|
|
1836
|
+
if not pae:
|
|
1837
|
+
return {}
|
|
1838
|
+
|
|
1839
|
+
# Flatten to get statistics
|
|
1840
|
+
all_values = []
|
|
1841
|
+
for model_pae in pae:
|
|
1842
|
+
for row in model_pae:
|
|
1843
|
+
all_values.extend(row)
|
|
1844
|
+
|
|
1845
|
+
return {
|
|
1846
|
+
"shape": f"{len(pae)}x{len(pae[0])}x{len(pae[0][0])}",
|
|
1847
|
+
"num_models": len(pae),
|
|
1848
|
+
"num_residues": len(pae[0]),
|
|
1849
|
+
"mean": sum(all_values) / len(all_values),
|
|
1850
|
+
"min": min(all_values),
|
|
1851
|
+
"max": max(all_values),
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
|
|
1855
|
+
def convert_cif_to_pdb(
|
|
1856
|
+
cif_input: Union[str, Path],
|
|
1857
|
+
pdb_output: Union[str, Path],
|
|
1858
|
+
structure_id: str = "PRED"
|
|
1859
|
+
) -> Path:
|
|
1860
|
+
"""
|
|
1861
|
+
Convert mmCIF file to PDB format.
|
|
1862
|
+
|
|
1863
|
+
Uses gemmi (preferred, faster) or BioPython as fallback.
|
|
1864
|
+
|
|
1865
|
+
Args:
|
|
1866
|
+
cif_input: Path to input CIF file or CIF content as string
|
|
1867
|
+
pdb_output: Path for output PDB file
|
|
1868
|
+
structure_id: Structure ID to use in PDB file (default: "PRED")
|
|
1869
|
+
|
|
1870
|
+
Returns:
|
|
1871
|
+
Path to the saved PDB file
|
|
1872
|
+
|
|
1873
|
+
Example:
|
|
1874
|
+
>>> convert_cif_to_pdb("structure.cif", "structure.pdb")
|
|
1875
|
+
>>> # Or from string content
|
|
1876
|
+
>>> convert_cif_to_pdb(cif_content, "structure.pdb")
|
|
1877
|
+
|
|
1878
|
+
Note:
|
|
1879
|
+
Requires either gemmi or BioPython to be installed:
|
|
1880
|
+
- pip install gemmi (recommended, faster)
|
|
1881
|
+
- pip install biopython
|
|
1882
|
+
"""
|
|
1883
|
+
import tempfile
|
|
1884
|
+
|
|
1885
|
+
pdb_output = Path(pdb_output)
|
|
1886
|
+
pdb_output.parent.mkdir(parents=True, exist_ok=True)
|
|
1887
|
+
|
|
1888
|
+
# Check if input is a file path or string content
|
|
1889
|
+
cif_path = Path(cif_input) if isinstance(cif_input, (str, Path)) and Path(cif_input).exists() else None
|
|
1890
|
+
|
|
1891
|
+
# If input is string content, write to temp file
|
|
1892
|
+
if cif_path is None:
|
|
1893
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.cif', delete=False) as f:
|
|
1894
|
+
f.write(str(cif_input))
|
|
1895
|
+
cif_path = Path(f.name)
|
|
1896
|
+
cleanup_temp = True
|
|
1897
|
+
else:
|
|
1898
|
+
cif_path = Path(cif_input)
|
|
1899
|
+
cleanup_temp = False
|
|
1900
|
+
|
|
1901
|
+
try:
|
|
1902
|
+
# Try gemmi first (faster)
|
|
1903
|
+
try:
|
|
1904
|
+
import gemmi
|
|
1905
|
+
structure = gemmi.read_structure(str(cif_path))
|
|
1906
|
+
structure.write_pdb(str(pdb_output))
|
|
1907
|
+
return pdb_output
|
|
1908
|
+
except ImportError:
|
|
1909
|
+
pass
|
|
1910
|
+
|
|
1911
|
+
# Fallback to BioPython
|
|
1912
|
+
try:
|
|
1913
|
+
from Bio.PDB import MMCIFParser, PDBIO
|
|
1914
|
+
import warnings
|
|
1915
|
+
|
|
1916
|
+
# Suppress BioPython warnings about incomplete structures
|
|
1917
|
+
with warnings.catch_warnings():
|
|
1918
|
+
warnings.simplefilter("ignore")
|
|
1919
|
+
|
|
1920
|
+
parser = MMCIFParser(QUIET=True)
|
|
1921
|
+
structure = parser.get_structure(structure_id, str(cif_path))
|
|
1922
|
+
|
|
1923
|
+
io = PDBIO()
|
|
1924
|
+
io.set_structure(structure)
|
|
1925
|
+
io.save(str(pdb_output))
|
|
1926
|
+
|
|
1927
|
+
return pdb_output
|
|
1928
|
+
except ImportError:
|
|
1929
|
+
raise ImportError(
|
|
1930
|
+
"CIF to PDB conversion requires either gemmi or BioPython. "
|
|
1931
|
+
"Install with: pip install gemmi (recommended) or pip install biopython"
|
|
1932
|
+
)
|
|
1933
|
+
finally:
|
|
1934
|
+
# Clean up temp file if created
|
|
1935
|
+
if cleanup_temp and cif_path.exists():
|
|
1936
|
+
cif_path.unlink()
|
|
1937
|
+
|
|
1938
|
+
|
|
1939
|
+
def convert_pdb_to_cif(
|
|
1940
|
+
pdb_input: Union[str, Path],
|
|
1941
|
+
cif_output: Union[str, Path],
|
|
1942
|
+
structure_id: str = "PRED"
|
|
1943
|
+
) -> Path:
|
|
1944
|
+
"""
|
|
1945
|
+
Convert PDB file to mmCIF format.
|
|
1946
|
+
|
|
1947
|
+
Uses gemmi (preferred, faster) or BioPython as fallback.
|
|
1948
|
+
|
|
1949
|
+
Args:
|
|
1950
|
+
pdb_input: Path to input PDB file or PDB content as string
|
|
1951
|
+
cif_output: Path for output CIF file
|
|
1952
|
+
structure_id: Structure ID to use (default: "PRED")
|
|
1953
|
+
|
|
1954
|
+
Returns:
|
|
1955
|
+
Path to the saved CIF file
|
|
1956
|
+
|
|
1957
|
+
Example:
|
|
1958
|
+
>>> convert_pdb_to_cif("structure.pdb", "structure.cif")
|
|
1959
|
+
"""
|
|
1960
|
+
import tempfile
|
|
1961
|
+
|
|
1962
|
+
cif_output = Path(cif_output)
|
|
1963
|
+
cif_output.parent.mkdir(parents=True, exist_ok=True)
|
|
1964
|
+
|
|
1965
|
+
# Check if input is a file path or string content
|
|
1966
|
+
pdb_path = Path(pdb_input) if isinstance(pdb_input, (str, Path)) and Path(pdb_input).exists() else None
|
|
1967
|
+
|
|
1968
|
+
# If input is string content, write to temp file
|
|
1969
|
+
if pdb_path is None:
|
|
1970
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.pdb', delete=False) as f:
|
|
1971
|
+
f.write(str(pdb_input))
|
|
1972
|
+
pdb_path = Path(f.name)
|
|
1973
|
+
cleanup_temp = True
|
|
1974
|
+
else:
|
|
1975
|
+
pdb_path = Path(pdb_input)
|
|
1976
|
+
cleanup_temp = False
|
|
1977
|
+
|
|
1978
|
+
try:
|
|
1979
|
+
# Try gemmi first (faster)
|
|
1980
|
+
try:
|
|
1981
|
+
import gemmi
|
|
1982
|
+
structure = gemmi.read_structure(str(pdb_path))
|
|
1983
|
+
structure.write_minimal_cif(str(cif_output))
|
|
1984
|
+
return cif_output
|
|
1985
|
+
except ImportError:
|
|
1986
|
+
pass
|
|
1987
|
+
|
|
1988
|
+
# Fallback to BioPython
|
|
1989
|
+
try:
|
|
1990
|
+
from Bio.PDB import PDBParser, MMCIFIO
|
|
1991
|
+
import warnings
|
|
1992
|
+
|
|
1993
|
+
with warnings.catch_warnings():
|
|
1994
|
+
warnings.simplefilter("ignore")
|
|
1995
|
+
|
|
1996
|
+
parser = PDBParser(QUIET=True)
|
|
1997
|
+
structure = parser.get_structure(structure_id, str(pdb_path))
|
|
1998
|
+
|
|
1999
|
+
io = MMCIFIO()
|
|
2000
|
+
io.set_structure(structure)
|
|
2001
|
+
io.save(str(cif_output))
|
|
2002
|
+
|
|
2003
|
+
return cif_output
|
|
2004
|
+
except ImportError:
|
|
2005
|
+
raise ImportError(
|
|
2006
|
+
"PDB to CIF conversion requires either gemmi or BioPython. "
|
|
2007
|
+
"Install with: pip install gemmi (recommended) or pip install biopython"
|
|
2008
|
+
)
|
|
2009
|
+
finally:
|
|
2010
|
+
if cleanup_temp and pdb_path.exists():
|
|
2011
|
+
pdb_path.unlink()
|
|
2012
|
+
|