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.
Files changed (112) hide show
  1. boltz/__init__.py +7 -0
  2. boltz/data/__init__.py +0 -0
  3. boltz/data/const.py +1184 -0
  4. boltz/data/crop/__init__.py +0 -0
  5. boltz/data/crop/affinity.py +164 -0
  6. boltz/data/crop/boltz.py +296 -0
  7. boltz/data/crop/cropper.py +45 -0
  8. boltz/data/feature/__init__.py +0 -0
  9. boltz/data/feature/featurizer.py +1230 -0
  10. boltz/data/feature/featurizerv2.py +2208 -0
  11. boltz/data/feature/symmetry.py +602 -0
  12. boltz/data/filter/__init__.py +0 -0
  13. boltz/data/filter/dynamic/__init__.py +0 -0
  14. boltz/data/filter/dynamic/date.py +76 -0
  15. boltz/data/filter/dynamic/filter.py +24 -0
  16. boltz/data/filter/dynamic/max_residues.py +37 -0
  17. boltz/data/filter/dynamic/resolution.py +34 -0
  18. boltz/data/filter/dynamic/size.py +38 -0
  19. boltz/data/filter/dynamic/subset.py +42 -0
  20. boltz/data/filter/static/__init__.py +0 -0
  21. boltz/data/filter/static/filter.py +26 -0
  22. boltz/data/filter/static/ligand.py +37 -0
  23. boltz/data/filter/static/polymer.py +299 -0
  24. boltz/data/module/__init__.py +0 -0
  25. boltz/data/module/inference.py +307 -0
  26. boltz/data/module/inferencev2.py +429 -0
  27. boltz/data/module/training.py +684 -0
  28. boltz/data/module/trainingv2.py +660 -0
  29. boltz/data/mol.py +900 -0
  30. boltz/data/msa/__init__.py +0 -0
  31. boltz/data/msa/mmseqs2.py +235 -0
  32. boltz/data/pad.py +84 -0
  33. boltz/data/parse/__init__.py +0 -0
  34. boltz/data/parse/a3m.py +134 -0
  35. boltz/data/parse/csv.py +100 -0
  36. boltz/data/parse/fasta.py +138 -0
  37. boltz/data/parse/mmcif.py +1239 -0
  38. boltz/data/parse/mmcif_with_constraints.py +1607 -0
  39. boltz/data/parse/schema.py +1851 -0
  40. boltz/data/parse/yaml.py +68 -0
  41. boltz/data/sample/__init__.py +0 -0
  42. boltz/data/sample/cluster.py +283 -0
  43. boltz/data/sample/distillation.py +57 -0
  44. boltz/data/sample/random.py +39 -0
  45. boltz/data/sample/sampler.py +49 -0
  46. boltz/data/tokenize/__init__.py +0 -0
  47. boltz/data/tokenize/boltz.py +195 -0
  48. boltz/data/tokenize/boltz2.py +396 -0
  49. boltz/data/tokenize/tokenizer.py +24 -0
  50. boltz/data/types.py +777 -0
  51. boltz/data/write/__init__.py +0 -0
  52. boltz/data/write/mmcif.py +305 -0
  53. boltz/data/write/pdb.py +171 -0
  54. boltz/data/write/utils.py +23 -0
  55. boltz/data/write/writer.py +330 -0
  56. boltz/main.py +1292 -0
  57. boltz/model/__init__.py +0 -0
  58. boltz/model/layers/__init__.py +0 -0
  59. boltz/model/layers/attention.py +132 -0
  60. boltz/model/layers/attentionv2.py +111 -0
  61. boltz/model/layers/confidence_utils.py +231 -0
  62. boltz/model/layers/dropout.py +34 -0
  63. boltz/model/layers/initialize.py +100 -0
  64. boltz/model/layers/outer_product_mean.py +98 -0
  65. boltz/model/layers/pair_averaging.py +135 -0
  66. boltz/model/layers/pairformer.py +337 -0
  67. boltz/model/layers/relative.py +58 -0
  68. boltz/model/layers/transition.py +78 -0
  69. boltz/model/layers/triangular_attention/__init__.py +0 -0
  70. boltz/model/layers/triangular_attention/attention.py +189 -0
  71. boltz/model/layers/triangular_attention/primitives.py +409 -0
  72. boltz/model/layers/triangular_attention/utils.py +380 -0
  73. boltz/model/layers/triangular_mult.py +212 -0
  74. boltz/model/loss/__init__.py +0 -0
  75. boltz/model/loss/bfactor.py +49 -0
  76. boltz/model/loss/confidence.py +590 -0
  77. boltz/model/loss/confidencev2.py +621 -0
  78. boltz/model/loss/diffusion.py +171 -0
  79. boltz/model/loss/diffusionv2.py +134 -0
  80. boltz/model/loss/distogram.py +48 -0
  81. boltz/model/loss/distogramv2.py +105 -0
  82. boltz/model/loss/validation.py +1025 -0
  83. boltz/model/models/__init__.py +0 -0
  84. boltz/model/models/boltz1.py +1286 -0
  85. boltz/model/models/boltz2.py +1249 -0
  86. boltz/model/modules/__init__.py +0 -0
  87. boltz/model/modules/affinity.py +223 -0
  88. boltz/model/modules/confidence.py +481 -0
  89. boltz/model/modules/confidence_utils.py +181 -0
  90. boltz/model/modules/confidencev2.py +495 -0
  91. boltz/model/modules/diffusion.py +844 -0
  92. boltz/model/modules/diffusion_conditioning.py +116 -0
  93. boltz/model/modules/diffusionv2.py +677 -0
  94. boltz/model/modules/encoders.py +639 -0
  95. boltz/model/modules/encodersv2.py +565 -0
  96. boltz/model/modules/transformers.py +322 -0
  97. boltz/model/modules/transformersv2.py +261 -0
  98. boltz/model/modules/trunk.py +688 -0
  99. boltz/model/modules/trunkv2.py +828 -0
  100. boltz/model/modules/utils.py +303 -0
  101. boltz/model/optim/__init__.py +0 -0
  102. boltz/model/optim/ema.py +389 -0
  103. boltz/model/optim/scheduler.py +99 -0
  104. boltz/model/potentials/__init__.py +0 -0
  105. boltz/model/potentials/potentials.py +497 -0
  106. boltz/model/potentials/schedules.py +32 -0
  107. boltz_vsynthes-1.0.0.dist-info/METADATA +151 -0
  108. boltz_vsynthes-1.0.0.dist-info/RECORD +112 -0
  109. boltz_vsynthes-1.0.0.dist-info/WHEEL +5 -0
  110. boltz_vsynthes-1.0.0.dist-info/entry_points.txt +2 -0
  111. boltz_vsynthes-1.0.0.dist-info/licenses/LICENSE +21 -0
  112. 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()
@@ -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