boltz-vsynthes 1.0.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.
- boltz/__init__.py +7 -0
- boltz/data/__init__.py +0 -0
- boltz/data/const.py +1184 -0
- boltz/data/crop/__init__.py +0 -0
- boltz/data/crop/affinity.py +164 -0
- boltz/data/crop/boltz.py +296 -0
- boltz/data/crop/cropper.py +45 -0
- boltz/data/feature/__init__.py +0 -0
- boltz/data/feature/featurizer.py +1230 -0
- boltz/data/feature/featurizerv2.py +2208 -0
- boltz/data/feature/symmetry.py +602 -0
- boltz/data/filter/__init__.py +0 -0
- boltz/data/filter/dynamic/__init__.py +0 -0
- boltz/data/filter/dynamic/date.py +76 -0
- boltz/data/filter/dynamic/filter.py +24 -0
- boltz/data/filter/dynamic/max_residues.py +37 -0
- boltz/data/filter/dynamic/resolution.py +34 -0
- boltz/data/filter/dynamic/size.py +38 -0
- boltz/data/filter/dynamic/subset.py +42 -0
- boltz/data/filter/static/__init__.py +0 -0
- boltz/data/filter/static/filter.py +26 -0
- boltz/data/filter/static/ligand.py +37 -0
- boltz/data/filter/static/polymer.py +299 -0
- boltz/data/module/__init__.py +0 -0
- boltz/data/module/inference.py +307 -0
- boltz/data/module/inferencev2.py +429 -0
- boltz/data/module/training.py +684 -0
- boltz/data/module/trainingv2.py +660 -0
- boltz/data/mol.py +900 -0
- boltz/data/msa/__init__.py +0 -0
- boltz/data/msa/mmseqs2.py +235 -0
- boltz/data/pad.py +84 -0
- boltz/data/parse/__init__.py +0 -0
- boltz/data/parse/a3m.py +134 -0
- boltz/data/parse/csv.py +100 -0
- boltz/data/parse/fasta.py +138 -0
- boltz/data/parse/mmcif.py +1239 -0
- boltz/data/parse/mmcif_with_constraints.py +1607 -0
- boltz/data/parse/schema.py +1851 -0
- boltz/data/parse/yaml.py +68 -0
- boltz/data/sample/__init__.py +0 -0
- boltz/data/sample/cluster.py +283 -0
- boltz/data/sample/distillation.py +57 -0
- boltz/data/sample/random.py +39 -0
- boltz/data/sample/sampler.py +49 -0
- boltz/data/tokenize/__init__.py +0 -0
- boltz/data/tokenize/boltz.py +195 -0
- boltz/data/tokenize/boltz2.py +396 -0
- boltz/data/tokenize/tokenizer.py +24 -0
- boltz/data/types.py +777 -0
- boltz/data/write/__init__.py +0 -0
- boltz/data/write/mmcif.py +305 -0
- boltz/data/write/pdb.py +171 -0
- boltz/data/write/utils.py +23 -0
- boltz/data/write/writer.py +330 -0
- boltz/main.py +1292 -0
- boltz/model/__init__.py +0 -0
- boltz/model/layers/__init__.py +0 -0
- boltz/model/layers/attention.py +132 -0
- boltz/model/layers/attentionv2.py +111 -0
- boltz/model/layers/confidence_utils.py +231 -0
- boltz/model/layers/dropout.py +34 -0
- boltz/model/layers/initialize.py +100 -0
- boltz/model/layers/outer_product_mean.py +98 -0
- boltz/model/layers/pair_averaging.py +135 -0
- boltz/model/layers/pairformer.py +337 -0
- boltz/model/layers/relative.py +58 -0
- boltz/model/layers/transition.py +78 -0
- boltz/model/layers/triangular_attention/__init__.py +0 -0
- boltz/model/layers/triangular_attention/attention.py +189 -0
- boltz/model/layers/triangular_attention/primitives.py +409 -0
- boltz/model/layers/triangular_attention/utils.py +380 -0
- boltz/model/layers/triangular_mult.py +212 -0
- boltz/model/loss/__init__.py +0 -0
- boltz/model/loss/bfactor.py +49 -0
- boltz/model/loss/confidence.py +590 -0
- boltz/model/loss/confidencev2.py +621 -0
- boltz/model/loss/diffusion.py +171 -0
- boltz/model/loss/diffusionv2.py +134 -0
- boltz/model/loss/distogram.py +48 -0
- boltz/model/loss/distogramv2.py +105 -0
- boltz/model/loss/validation.py +1025 -0
- boltz/model/models/__init__.py +0 -0
- boltz/model/models/boltz1.py +1286 -0
- boltz/model/models/boltz2.py +1249 -0
- boltz/model/modules/__init__.py +0 -0
- boltz/model/modules/affinity.py +223 -0
- boltz/model/modules/confidence.py +481 -0
- boltz/model/modules/confidence_utils.py +181 -0
- boltz/model/modules/confidencev2.py +495 -0
- boltz/model/modules/diffusion.py +844 -0
- boltz/model/modules/diffusion_conditioning.py +116 -0
- boltz/model/modules/diffusionv2.py +677 -0
- boltz/model/modules/encoders.py +639 -0
- boltz/model/modules/encodersv2.py +565 -0
- boltz/model/modules/transformers.py +322 -0
- boltz/model/modules/transformersv2.py +261 -0
- boltz/model/modules/trunk.py +688 -0
- boltz/model/modules/trunkv2.py +828 -0
- boltz/model/modules/utils.py +303 -0
- boltz/model/optim/__init__.py +0 -0
- boltz/model/optim/ema.py +389 -0
- boltz/model/optim/scheduler.py +99 -0
- boltz/model/potentials/__init__.py +0 -0
- boltz/model/potentials/potentials.py +497 -0
- boltz/model/potentials/schedules.py +32 -0
- boltz_vsynthes-1.0.0.dist-info/METADATA +151 -0
- boltz_vsynthes-1.0.0.dist-info/RECORD +112 -0
- boltz_vsynthes-1.0.0.dist-info/WHEEL +5 -0
- boltz_vsynthes-1.0.0.dist-info/entry_points.txt +2 -0
- boltz_vsynthes-1.0.0.dist-info/licenses/LICENSE +21 -0
- boltz_vsynthes-1.0.0.dist-info/top_level.txt +1 -0
File without changes
|
@@ -0,0 +1,305 @@
|
|
1
|
+
import io
|
2
|
+
import re
|
3
|
+
from collections.abc import Iterator
|
4
|
+
from typing import Optional
|
5
|
+
|
6
|
+
import ihm
|
7
|
+
import modelcif
|
8
|
+
from modelcif import Assembly, AsymUnit, Entity, System, dumper
|
9
|
+
from modelcif.model import AbInitioModel, Atom, ModelGroup
|
10
|
+
from rdkit import Chem
|
11
|
+
from torch import Tensor
|
12
|
+
|
13
|
+
from boltz.data import const
|
14
|
+
from boltz.data.types import Structure
|
15
|
+
|
16
|
+
|
17
|
+
def to_mmcif(
|
18
|
+
structure: Structure,
|
19
|
+
plddts: Optional[Tensor] = None,
|
20
|
+
boltz2: bool = False,
|
21
|
+
) -> str: # noqa: C901, PLR0915, PLR0912
|
22
|
+
"""Write a structure into an MMCIF file.
|
23
|
+
|
24
|
+
Parameters
|
25
|
+
----------
|
26
|
+
structure : Structure
|
27
|
+
The input structure
|
28
|
+
|
29
|
+
Returns
|
30
|
+
-------
|
31
|
+
str
|
32
|
+
the output MMCIF file
|
33
|
+
|
34
|
+
"""
|
35
|
+
system = System()
|
36
|
+
|
37
|
+
# Load periodic table for element mapping
|
38
|
+
periodic_table = Chem.GetPeriodicTable()
|
39
|
+
|
40
|
+
# Map entities to chain_ids
|
41
|
+
entity_to_chains = {}
|
42
|
+
entity_to_moltype = {}
|
43
|
+
|
44
|
+
for chain in structure.chains:
|
45
|
+
entity_id = chain["entity_id"]
|
46
|
+
mol_type = chain["mol_type"]
|
47
|
+
entity_to_chains.setdefault(entity_id, []).append(chain)
|
48
|
+
entity_to_moltype[entity_id] = mol_type
|
49
|
+
|
50
|
+
# Map entities to sequences
|
51
|
+
sequences = {}
|
52
|
+
for entity in entity_to_chains:
|
53
|
+
# Get the first chain
|
54
|
+
chain = entity_to_chains[entity][0]
|
55
|
+
|
56
|
+
# Get the sequence
|
57
|
+
res_start = chain["res_idx"]
|
58
|
+
res_end = chain["res_idx"] + chain["res_num"]
|
59
|
+
residues = structure.residues[res_start:res_end]
|
60
|
+
sequence = [str(res["name"]) for res in residues]
|
61
|
+
sequences[entity] = sequence
|
62
|
+
|
63
|
+
# Create entity objects
|
64
|
+
lig_entity = None
|
65
|
+
entities_map = {}
|
66
|
+
for entity, sequence in sequences.items():
|
67
|
+
mol_type = entity_to_moltype[entity]
|
68
|
+
|
69
|
+
if mol_type == const.chain_type_ids["PROTEIN"]:
|
70
|
+
alphabet = ihm.LPeptideAlphabet()
|
71
|
+
chem_comp = lambda x: ihm.LPeptideChemComp(id=x, code=x, code_canonical="X") # noqa: E731
|
72
|
+
elif mol_type == const.chain_type_ids["DNA"]:
|
73
|
+
alphabet = ihm.DNAAlphabet()
|
74
|
+
chem_comp = lambda x: ihm.DNAChemComp(id=x, code=x, code_canonical="N") # noqa: E731
|
75
|
+
elif mol_type == const.chain_type_ids["RNA"]:
|
76
|
+
alphabet = ihm.RNAAlphabet()
|
77
|
+
chem_comp = lambda x: ihm.RNAChemComp(id=x, code=x, code_canonical="N") # noqa: E731
|
78
|
+
elif len(sequence) > 1:
|
79
|
+
alphabet = {}
|
80
|
+
chem_comp = lambda x: ihm.SaccharideChemComp(id=x) # noqa: E731
|
81
|
+
else:
|
82
|
+
alphabet = {}
|
83
|
+
chem_comp = lambda x: ihm.NonPolymerChemComp(id=x) # noqa: E731
|
84
|
+
|
85
|
+
# Handle smiles
|
86
|
+
if len(sequence) == 1 and (sequence[0] == "LIG"):
|
87
|
+
if lig_entity is None:
|
88
|
+
seq = [chem_comp(sequence[0])]
|
89
|
+
lig_entity = Entity(seq)
|
90
|
+
model_e = lig_entity
|
91
|
+
else:
|
92
|
+
seq = [
|
93
|
+
alphabet[item] if item in alphabet else chem_comp(item)
|
94
|
+
for item in sequence
|
95
|
+
]
|
96
|
+
model_e = Entity(seq)
|
97
|
+
|
98
|
+
for chain in entity_to_chains[entity]:
|
99
|
+
chain_idx = chain["asym_id"]
|
100
|
+
entities_map[chain_idx] = model_e
|
101
|
+
|
102
|
+
# We don't assume that symmetry is perfect, so we dump everything
|
103
|
+
# into the asymmetric unit, and produce just a single assembly
|
104
|
+
asym_unit_map = {}
|
105
|
+
for chain in structure.chains:
|
106
|
+
# Define the model assembly
|
107
|
+
chain_idx = chain["asym_id"]
|
108
|
+
chain_tag = str(chain["name"])
|
109
|
+
entity = entities_map[chain_idx]
|
110
|
+
if entity.type == "water":
|
111
|
+
asym = ihm.WaterAsymUnit(
|
112
|
+
entity,
|
113
|
+
1,
|
114
|
+
details="Model subunit %s" % chain_tag,
|
115
|
+
id=chain_tag,
|
116
|
+
)
|
117
|
+
else:
|
118
|
+
asym = AsymUnit(
|
119
|
+
entity,
|
120
|
+
details="Model subunit %s" % chain_tag,
|
121
|
+
id=chain_tag,
|
122
|
+
)
|
123
|
+
asym_unit_map[chain_idx] = asym
|
124
|
+
modeled_assembly = Assembly(asym_unit_map.values(), name="Modeled assembly")
|
125
|
+
|
126
|
+
class _LocalPLDDT(modelcif.qa_metric.Local, modelcif.qa_metric.PLDDT):
|
127
|
+
name = "pLDDT"
|
128
|
+
software = None
|
129
|
+
description = "Predicted lddt"
|
130
|
+
|
131
|
+
class _MyModel(AbInitioModel):
|
132
|
+
def get_atoms(self) -> Iterator[Atom]:
|
133
|
+
# Index into plddt tensor for current residue.
|
134
|
+
res_num = 0
|
135
|
+
# Tracks non-ligand plddt tensor indices,
|
136
|
+
# Initializing to -1 handles case where ligand is resnum 0
|
137
|
+
prev_polymer_resnum = -1
|
138
|
+
# Tracks ligand indices.
|
139
|
+
ligand_index_offset = 0
|
140
|
+
|
141
|
+
# Add all atom sites.
|
142
|
+
for chain in structure.chains:
|
143
|
+
# We rename the chains in alphabetical order
|
144
|
+
het = chain["mol_type"] == const.chain_type_ids["NONPOLYMER"]
|
145
|
+
chain_idx = chain["asym_id"]
|
146
|
+
res_start = chain["res_idx"]
|
147
|
+
res_end = chain["res_idx"] + chain["res_num"]
|
148
|
+
|
149
|
+
record_type = (
|
150
|
+
"ATOM"
|
151
|
+
if chain["mol_type"] != const.chain_type_ids["NONPOLYMER"]
|
152
|
+
else "HETATM"
|
153
|
+
)
|
154
|
+
|
155
|
+
residues = structure.residues[res_start:res_end]
|
156
|
+
for residue in residues:
|
157
|
+
res_name = str(residue["name"])
|
158
|
+
atom_start = residue["atom_idx"]
|
159
|
+
atom_end = residue["atom_idx"] + residue["atom_num"]
|
160
|
+
atoms = structure.atoms[atom_start:atom_end]
|
161
|
+
atom_coords = atoms["coords"]
|
162
|
+
for i, atom in enumerate(atoms):
|
163
|
+
# This should not happen on predictions, but just in case.
|
164
|
+
if not atom["is_present"]:
|
165
|
+
continue
|
166
|
+
|
167
|
+
if boltz2:
|
168
|
+
atom_name = str(atom["name"])
|
169
|
+
atom_key = re.sub(r"\d", "", atom_name)
|
170
|
+
if atom_key in const.ambiguous_atoms:
|
171
|
+
if isinstance(const.ambiguous_atoms[atom_key], str):
|
172
|
+
element = const.ambiguous_atoms[atom_key]
|
173
|
+
elif res_name in const.ambiguous_atoms[atom_key]:
|
174
|
+
element = const.ambiguous_atoms[atom_key][res_name]
|
175
|
+
else:
|
176
|
+
element = const.ambiguous_atoms[atom_key]["*"]
|
177
|
+
else:
|
178
|
+
element = atom_key[0]
|
179
|
+
else:
|
180
|
+
atom_name = atom["name"]
|
181
|
+
atom_name = [chr(c + 32) for c in atom_name if c != 0]
|
182
|
+
atom_name = "".join(atom_name)
|
183
|
+
element = periodic_table.GetElementSymbol(
|
184
|
+
atom["element"].item()
|
185
|
+
)
|
186
|
+
element = element.upper()
|
187
|
+
residue_index = residue["res_idx"] + 1
|
188
|
+
pos = atom_coords[i]
|
189
|
+
|
190
|
+
if record_type != "HETATM":
|
191
|
+
# The current residue plddt is stored at the res_num index unless a ligand has previouly been added.
|
192
|
+
biso = (
|
193
|
+
100.00
|
194
|
+
if plddts is None
|
195
|
+
else round(
|
196
|
+
plddts[res_num + ligand_index_offset].item() * 100,
|
197
|
+
3,
|
198
|
+
)
|
199
|
+
)
|
200
|
+
prev_polymer_resnum = res_num
|
201
|
+
else:
|
202
|
+
# If not a polymer resnum, we can get index into plddts by adding offset relative to previous polymer resnum.
|
203
|
+
ligand_index_offset += 1
|
204
|
+
biso = (
|
205
|
+
100.00
|
206
|
+
if plddts is None
|
207
|
+
else round(
|
208
|
+
plddts[
|
209
|
+
prev_polymer_resnum + ligand_index_offset
|
210
|
+
].item()
|
211
|
+
* 100,
|
212
|
+
3,
|
213
|
+
)
|
214
|
+
)
|
215
|
+
|
216
|
+
yield Atom(
|
217
|
+
asym_unit=asym_unit_map[chain_idx],
|
218
|
+
type_symbol=element,
|
219
|
+
seq_id=residue_index,
|
220
|
+
atom_id=atom_name,
|
221
|
+
x=f"{pos[0]:.5f}",
|
222
|
+
y=f"{pos[1]:.5f}",
|
223
|
+
z=f"{pos[2]:.5f}",
|
224
|
+
het=het,
|
225
|
+
biso=biso,
|
226
|
+
occupancy=1,
|
227
|
+
)
|
228
|
+
|
229
|
+
if record_type != "HETATM":
|
230
|
+
res_num += 1
|
231
|
+
|
232
|
+
def add_plddt(self, plddts):
|
233
|
+
res_num = 0
|
234
|
+
prev_polymer_resnum = (
|
235
|
+
-1
|
236
|
+
) # -1 handles case where ligand is the first residue
|
237
|
+
ligand_index_offset = 0
|
238
|
+
for chain in structure.chains:
|
239
|
+
chain_idx = chain["asym_id"]
|
240
|
+
res_start = chain["res_idx"]
|
241
|
+
res_end = chain["res_idx"] + chain["res_num"]
|
242
|
+
residues = structure.residues[res_start:res_end]
|
243
|
+
|
244
|
+
record_type = (
|
245
|
+
"ATOM"
|
246
|
+
if chain["mol_type"] != const.chain_type_ids["NONPOLYMER"]
|
247
|
+
else "HETATM"
|
248
|
+
)
|
249
|
+
|
250
|
+
# We rename the chains in alphabetical order
|
251
|
+
for residue in residues:
|
252
|
+
residue_idx = residue["res_idx"] + 1
|
253
|
+
|
254
|
+
atom_start = residue["atom_idx"]
|
255
|
+
atom_end = residue["atom_idx"] + residue["atom_num"]
|
256
|
+
|
257
|
+
if record_type != "HETATM":
|
258
|
+
# The current residue plddt is stored at the res_num index unless a ligand has previouly been added.
|
259
|
+
self.qa_metrics.append(
|
260
|
+
_LocalPLDDT(
|
261
|
+
asym_unit_map[chain_idx].residue(residue_idx),
|
262
|
+
round(
|
263
|
+
plddts[res_num + ligand_index_offset].item() * 100,
|
264
|
+
3,
|
265
|
+
),
|
266
|
+
)
|
267
|
+
)
|
268
|
+
prev_polymer_resnum = res_num
|
269
|
+
else:
|
270
|
+
# If not a polymer resnum, we can get index into plddts by adding offset relative to previous polymer resnum.
|
271
|
+
self.qa_metrics.append(
|
272
|
+
_LocalPLDDT(
|
273
|
+
asym_unit_map[chain_idx].residue(residue_idx),
|
274
|
+
round(
|
275
|
+
plddts[
|
276
|
+
prev_polymer_resnum
|
277
|
+
+ ligand_index_offset
|
278
|
+
+ 1 : prev_polymer_resnum
|
279
|
+
+ ligand_index_offset
|
280
|
+
+ residue["atom_num"]
|
281
|
+
+ 1
|
282
|
+
]
|
283
|
+
.mean()
|
284
|
+
.item()
|
285
|
+
* 100,
|
286
|
+
2,
|
287
|
+
),
|
288
|
+
)
|
289
|
+
)
|
290
|
+
ligand_index_offset += residue["atom_num"]
|
291
|
+
|
292
|
+
if record_type != "HETATM":
|
293
|
+
res_num += 1
|
294
|
+
|
295
|
+
# Add the model and modeling protocol to the file and write them out:
|
296
|
+
model = _MyModel(assembly=modeled_assembly, name="Model")
|
297
|
+
if plddts is not None:
|
298
|
+
model.add_plddt(plddts)
|
299
|
+
|
300
|
+
model_group = ModelGroup([model], name="All models")
|
301
|
+
system.model_groups.append(model_group)
|
302
|
+
|
303
|
+
fh = io.StringIO()
|
304
|
+
dumper.write(fh, [system])
|
305
|
+
return fh.getvalue()
|
boltz/data/write/pdb.py
ADDED
@@ -0,0 +1,171 @@
|
|
1
|
+
import re
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
from rdkit import Chem
|
5
|
+
from torch import Tensor
|
6
|
+
|
7
|
+
from boltz.data import const
|
8
|
+
from boltz.data.types import Structure
|
9
|
+
|
10
|
+
|
11
|
+
def to_pdb(
|
12
|
+
structure: Structure,
|
13
|
+
plddts: Optional[Tensor] = None,
|
14
|
+
boltz2: bool = False,
|
15
|
+
) -> str: # noqa: PLR0915
|
16
|
+
"""Write a structure into a PDB file.
|
17
|
+
|
18
|
+
Parameters
|
19
|
+
----------
|
20
|
+
structure : Structure
|
21
|
+
The input structure
|
22
|
+
|
23
|
+
Returns
|
24
|
+
-------
|
25
|
+
str
|
26
|
+
the output PDB file
|
27
|
+
|
28
|
+
"""
|
29
|
+
pdb_lines = []
|
30
|
+
|
31
|
+
atom_index = 1
|
32
|
+
atom_reindex_ter = []
|
33
|
+
|
34
|
+
# Load periodic table for element mapping
|
35
|
+
periodic_table = Chem.GetPeriodicTable()
|
36
|
+
|
37
|
+
# Index into plddt tensor for current residue.
|
38
|
+
res_num = 0
|
39
|
+
# Tracks non-ligand plddt tensor indices,
|
40
|
+
# Initializing to -1 handles case where ligand is resnum 0
|
41
|
+
prev_polymer_resnum = -1
|
42
|
+
# Tracks ligand indices.
|
43
|
+
ligand_index_offset = 0
|
44
|
+
|
45
|
+
# Add all atom sites.
|
46
|
+
for chain in structure.chains:
|
47
|
+
# We rename the chains in alphabetical order
|
48
|
+
chain_idx = chain["asym_id"]
|
49
|
+
chain_tag = chain["name"]
|
50
|
+
|
51
|
+
res_start = chain["res_idx"]
|
52
|
+
res_end = chain["res_idx"] + chain["res_num"]
|
53
|
+
|
54
|
+
residues = structure.residues[res_start:res_end]
|
55
|
+
for residue in residues:
|
56
|
+
res_name = str(residue["name"])
|
57
|
+
atom_start = residue["atom_idx"]
|
58
|
+
atom_end = residue["atom_idx"] + residue["atom_num"]
|
59
|
+
atoms = structure.atoms[atom_start:atom_end]
|
60
|
+
atom_coords = atoms["coords"]
|
61
|
+
for i, atom in enumerate(atoms):
|
62
|
+
# This should not happen on predictions, but just in case.
|
63
|
+
if not atom["is_present"]:
|
64
|
+
continue
|
65
|
+
|
66
|
+
record_type = (
|
67
|
+
"ATOM"
|
68
|
+
if chain["mol_type"] != const.chain_type_ids["NONPOLYMER"]
|
69
|
+
else "HETATM"
|
70
|
+
)
|
71
|
+
name = str(atom["name"])
|
72
|
+
if boltz2:
|
73
|
+
atom_name = str(atom["name"])
|
74
|
+
atom_key = re.sub(r"\d", "", atom_name)
|
75
|
+
if atom_key in const.ambiguous_atoms:
|
76
|
+
if isinstance(const.ambiguous_atoms[atom_key], str):
|
77
|
+
element = const.ambiguous_atoms[atom_key]
|
78
|
+
elif res_name in const.ambiguous_atoms[atom_key]:
|
79
|
+
element = const.ambiguous_atoms[atom_key][res_name]
|
80
|
+
else:
|
81
|
+
element = const.ambiguous_atoms[atom_key]["*"]
|
82
|
+
else:
|
83
|
+
element = atom_key[0]
|
84
|
+
else:
|
85
|
+
atom_name = atom["name"]
|
86
|
+
atom_name = [chr(c + 32) for c in atom_name if c != 0]
|
87
|
+
atom_name = "".join(atom_name)
|
88
|
+
element = periodic_table.GetElementSymbol(atom["element"].item())
|
89
|
+
|
90
|
+
name = name if len(name) == 4 else f" {name}" # noqa: PLR2004
|
91
|
+
alt_loc = ""
|
92
|
+
insertion_code = ""
|
93
|
+
occupancy = 1.00
|
94
|
+
element = element.upper()
|
95
|
+
charge = ""
|
96
|
+
residue_index = residue["res_idx"] + 1
|
97
|
+
pos = atom_coords[i]
|
98
|
+
res_name_3 = (
|
99
|
+
"LIG" if record_type == "HETATM" else str(residue["name"][:3])
|
100
|
+
)
|
101
|
+
|
102
|
+
if record_type != "HETATM":
|
103
|
+
# The current residue plddt is stored at the res_num index unless a ligand has previouly been added.
|
104
|
+
b_factor = (
|
105
|
+
100.00
|
106
|
+
if plddts is None
|
107
|
+
else round(
|
108
|
+
plddts[res_num + ligand_index_offset].item() * 100, 2
|
109
|
+
)
|
110
|
+
)
|
111
|
+
prev_polymer_resnum = res_num
|
112
|
+
else:
|
113
|
+
# If not a polymer resnum, we can get index into plddts by adding offset relative to previous polymer resnum.
|
114
|
+
ligand_index_offset += 1
|
115
|
+
b_factor = (
|
116
|
+
100.00
|
117
|
+
if plddts is None
|
118
|
+
else round(
|
119
|
+
plddts[prev_polymer_resnum + ligand_index_offset].item()
|
120
|
+
* 100,
|
121
|
+
2,
|
122
|
+
)
|
123
|
+
)
|
124
|
+
|
125
|
+
# PDB is a columnar format, every space matters here!
|
126
|
+
atom_line = (
|
127
|
+
f"{record_type:<6}{atom_index:>5} {name:<4}{alt_loc:>1}"
|
128
|
+
f"{res_name_3:>3} {chain_tag:>1}"
|
129
|
+
f"{residue_index:>4}{insertion_code:>1} "
|
130
|
+
f"{pos[0]:>8.3f}{pos[1]:>8.3f}{pos[2]:>8.3f}"
|
131
|
+
f"{occupancy:>6.2f}{b_factor:>6.2f} "
|
132
|
+
f"{element:>2}{charge:>2}"
|
133
|
+
)
|
134
|
+
pdb_lines.append(atom_line)
|
135
|
+
atom_reindex_ter.append(atom_index)
|
136
|
+
atom_index += 1
|
137
|
+
|
138
|
+
if record_type != "HETATM":
|
139
|
+
res_num += 1
|
140
|
+
|
141
|
+
should_terminate = chain_idx < (len(structure.chains) - 1)
|
142
|
+
if should_terminate:
|
143
|
+
# Close the chain.
|
144
|
+
chain_end = "TER"
|
145
|
+
chain_termination_line = (
|
146
|
+
f"{chain_end:<6}{atom_index:>5} "
|
147
|
+
f"{res_name_3:>3} "
|
148
|
+
f"{chain_tag:>1}{residue_index:>4}"
|
149
|
+
)
|
150
|
+
pdb_lines.append(chain_termination_line)
|
151
|
+
atom_index += 1
|
152
|
+
|
153
|
+
# Dump CONECT records.
|
154
|
+
all_bonds = structure.bonds
|
155
|
+
if hasattr(structure, "connections"):
|
156
|
+
all_bonds = all_bonds + structure.connections
|
157
|
+
|
158
|
+
for bond in all_bonds:
|
159
|
+
atom1 = structure.atoms[bond["atom_1"]]
|
160
|
+
atom2 = structure.atoms[bond["atom_2"]]
|
161
|
+
if not atom1["is_present"] or not atom2["is_present"]:
|
162
|
+
continue
|
163
|
+
atom1_idx = atom_reindex_ter[bond["atom_1"]]
|
164
|
+
atom2_idx = atom_reindex_ter[bond["atom_2"]]
|
165
|
+
conect_line = f"CONECT{atom1_idx:>5}{atom2_idx:>5}"
|
166
|
+
pdb_lines.append(conect_line)
|
167
|
+
|
168
|
+
pdb_lines.append("END")
|
169
|
+
pdb_lines.append("")
|
170
|
+
pdb_lines = [line.ljust(80) for line in pdb_lines]
|
171
|
+
return "\n".join(pdb_lines)
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import string
|
2
|
+
from collections.abc import Iterator
|
3
|
+
|
4
|
+
|
5
|
+
def generate_tags() -> Iterator[str]:
|
6
|
+
"""Generate chain tags.
|
7
|
+
|
8
|
+
Yields
|
9
|
+
------
|
10
|
+
str
|
11
|
+
The next chain tag
|
12
|
+
|
13
|
+
"""
|
14
|
+
for i in range(1, 4):
|
15
|
+
for j in range(len(string.ascii_uppercase) ** i):
|
16
|
+
tag = ""
|
17
|
+
for k in range(i):
|
18
|
+
tag += string.ascii_uppercase[
|
19
|
+
j
|
20
|
+
// (len(string.ascii_uppercase) ** k)
|
21
|
+
% len(string.ascii_uppercase)
|
22
|
+
]
|
23
|
+
yield tag
|