molbuilder 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.
- molbuilder/__init__.py +8 -0
- molbuilder/__main__.py +6 -0
- molbuilder/atomic/__init__.py +4 -0
- molbuilder/atomic/bohr.py +235 -0
- molbuilder/atomic/quantum_atom.py +334 -0
- molbuilder/atomic/quantum_numbers.py +196 -0
- molbuilder/atomic/wavefunctions.py +297 -0
- molbuilder/bonding/__init__.py +4 -0
- molbuilder/bonding/covalent.py +442 -0
- molbuilder/bonding/lewis.py +347 -0
- molbuilder/bonding/vsepr.py +433 -0
- molbuilder/cli/__init__.py +1 -0
- molbuilder/cli/demos.py +516 -0
- molbuilder/cli/menu.py +127 -0
- molbuilder/cli/wizard.py +831 -0
- molbuilder/core/__init__.py +6 -0
- molbuilder/core/bond_data.py +170 -0
- molbuilder/core/constants.py +51 -0
- molbuilder/core/element_properties.py +183 -0
- molbuilder/core/elements.py +181 -0
- molbuilder/core/geometry.py +232 -0
- molbuilder/gui/__init__.py +2 -0
- molbuilder/gui/app.py +286 -0
- molbuilder/gui/canvas3d.py +115 -0
- molbuilder/gui/dialogs.py +117 -0
- molbuilder/gui/event_handler.py +118 -0
- molbuilder/gui/sidebar.py +105 -0
- molbuilder/gui/toolbar.py +71 -0
- molbuilder/io/__init__.py +1 -0
- molbuilder/io/json_io.py +146 -0
- molbuilder/io/mol_sdf.py +169 -0
- molbuilder/io/pdb.py +184 -0
- molbuilder/io/smiles_io.py +47 -0
- molbuilder/io/xyz.py +103 -0
- molbuilder/molecule/__init__.py +2 -0
- molbuilder/molecule/amino_acids.py +919 -0
- molbuilder/molecule/builders.py +257 -0
- molbuilder/molecule/conformations.py +70 -0
- molbuilder/molecule/functional_groups.py +484 -0
- molbuilder/molecule/graph.py +712 -0
- molbuilder/molecule/peptides.py +13 -0
- molbuilder/molecule/stereochemistry.py +6 -0
- molbuilder/process/__init__.py +3 -0
- molbuilder/process/conditions.py +260 -0
- molbuilder/process/costing.py +316 -0
- molbuilder/process/purification.py +285 -0
- molbuilder/process/reactor.py +297 -0
- molbuilder/process/safety.py +476 -0
- molbuilder/process/scale_up.py +427 -0
- molbuilder/process/solvent_systems.py +204 -0
- molbuilder/reactions/__init__.py +3 -0
- molbuilder/reactions/functional_group_detect.py +728 -0
- molbuilder/reactions/knowledge_base.py +1716 -0
- molbuilder/reactions/reaction_types.py +102 -0
- molbuilder/reactions/reagent_data.py +1248 -0
- molbuilder/reactions/retrosynthesis.py +1430 -0
- molbuilder/reactions/synthesis_route.py +377 -0
- molbuilder/reports/__init__.py +158 -0
- molbuilder/reports/cost_report.py +206 -0
- molbuilder/reports/molecule_report.py +279 -0
- molbuilder/reports/safety_report.py +296 -0
- molbuilder/reports/synthesis_report.py +283 -0
- molbuilder/reports/text_formatter.py +170 -0
- molbuilder/smiles/__init__.py +4 -0
- molbuilder/smiles/parser.py +487 -0
- molbuilder/smiles/tokenizer.py +291 -0
- molbuilder/smiles/writer.py +375 -0
- molbuilder/visualization/__init__.py +1 -0
- molbuilder/visualization/bohr_viz.py +166 -0
- molbuilder/visualization/molecule_viz.py +368 -0
- molbuilder/visualization/quantum_viz.py +434 -0
- molbuilder/visualization/theme.py +12 -0
- molbuilder-1.0.0.dist-info/METADATA +360 -0
- molbuilder-1.0.0.dist-info/RECORD +78 -0
- molbuilder-1.0.0.dist-info/WHEEL +5 -0
- molbuilder-1.0.0.dist-info/entry_points.txt +2 -0
- molbuilder-1.0.0.dist-info/licenses/LICENSE +21 -0
- molbuilder-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Functional group builder functions.
|
|
3
|
+
|
|
4
|
+
Each function attaches a functional group to an existing atom in a
|
|
5
|
+
Molecule and returns a dict mapping atom labels to their new indices.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import math
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
from molbuilder.molecule.graph import Molecule, Hybridization
|
|
15
|
+
from molbuilder.core.bond_data import bond_length, SP3_ANGLE, SP2_ANGLE, SP_ANGLE
|
|
16
|
+
from molbuilder.core.geometry import normalize, available_tetrahedral_dirs, place_atom_zmatrix
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ===================================================================
|
|
20
|
+
# Constants
|
|
21
|
+
# ===================================================================
|
|
22
|
+
|
|
23
|
+
# Bond lengths not covered by STANDARD_BOND_LENGTHS (Angstroms)
|
|
24
|
+
AMIDE_CN = 1.33 # amide C-N (partial double bond character)
|
|
25
|
+
SS_BOND = 2.05 # disulfide S-S
|
|
26
|
+
CC_AROMATIC = 1.40 # aromatic C-C (average)
|
|
27
|
+
CN_AROMATIC = 1.34 # aromatic C-N (average)
|
|
28
|
+
CO_CARBOXYL = 1.25 # carboxylate C-O (resonance average)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def add_hydroxyl(mol: Molecule, attach_to: int,
|
|
32
|
+
angle_ref: int | None = None,
|
|
33
|
+
dihedral_ref: int | None = None,
|
|
34
|
+
dihedral_deg: float = 180.0) -> dict[str, int]:
|
|
35
|
+
"""Attach -OH to an atom. Returns {'O': idx, 'H': idx}."""
|
|
36
|
+
if angle_ref is None:
|
|
37
|
+
nbrs = mol.neighbors(attach_to)
|
|
38
|
+
angle_ref = nbrs[0] if nbrs else 0
|
|
39
|
+
|
|
40
|
+
o_idx = mol.add_atom_bonded(
|
|
41
|
+
"O", attach_to, bond_order=1, angle_ref=angle_ref,
|
|
42
|
+
dihedral_ref=dihedral_ref, dihedral_deg=dihedral_deg,
|
|
43
|
+
hybridization=Hybridization.SP3)
|
|
44
|
+
|
|
45
|
+
h_idx = mol.add_atom_bonded(
|
|
46
|
+
"H", o_idx, bond_order=1, angle_ref=attach_to,
|
|
47
|
+
bond_angle_deg=SP3_ANGLE, dihedral_deg=180.0,
|
|
48
|
+
rotatable=False)
|
|
49
|
+
|
|
50
|
+
return {"O": o_idx, "H": h_idx}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def add_amino(mol: Molecule, attach_to: int,
|
|
54
|
+
angle_ref: int | None = None,
|
|
55
|
+
dihedral_ref: int | None = None,
|
|
56
|
+
dihedral_deg: float = 180.0) -> dict[str, int]:
|
|
57
|
+
"""Attach -NH2 to an atom. Returns {'N': idx, 'H1': idx, 'H2': idx}."""
|
|
58
|
+
if angle_ref is None:
|
|
59
|
+
nbrs = mol.neighbors(attach_to)
|
|
60
|
+
angle_ref = nbrs[0] if nbrs else 0
|
|
61
|
+
|
|
62
|
+
n_idx = mol.add_atom_bonded(
|
|
63
|
+
"N", attach_to, bond_order=1, angle_ref=angle_ref,
|
|
64
|
+
dihedral_ref=dihedral_ref, dihedral_deg=dihedral_deg,
|
|
65
|
+
hybridization=Hybridization.SP3)
|
|
66
|
+
|
|
67
|
+
h1 = mol.add_atom_bonded(
|
|
68
|
+
"H", n_idx, bond_order=1, angle_ref=attach_to,
|
|
69
|
+
bond_angle_deg=SP3_ANGLE, dihedral_deg=120.0,
|
|
70
|
+
rotatable=False)
|
|
71
|
+
|
|
72
|
+
h2 = mol.add_atom_bonded(
|
|
73
|
+
"H", n_idx, bond_order=1, angle_ref=attach_to,
|
|
74
|
+
bond_angle_deg=SP3_ANGLE, dihedral_deg=-120.0,
|
|
75
|
+
rotatable=False)
|
|
76
|
+
|
|
77
|
+
return {"N": n_idx, "H1": h1, "H2": h2}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def add_carboxyl(mol: Molecule, attach_to: int,
|
|
81
|
+
angle_ref: int | None = None,
|
|
82
|
+
dihedral_ref: int | None = None,
|
|
83
|
+
dihedral_deg: float = 180.0) -> dict[str, int]:
|
|
84
|
+
"""Attach -COOH to an atom. Returns {'C': idx, 'O1': idx, 'O2': idx, 'H': idx}."""
|
|
85
|
+
if angle_ref is None:
|
|
86
|
+
nbrs = mol.neighbors(attach_to)
|
|
87
|
+
angle_ref = nbrs[0] if nbrs else 0
|
|
88
|
+
|
|
89
|
+
c_idx = mol.add_atom_bonded(
|
|
90
|
+
"C", attach_to, bond_order=1, angle_ref=angle_ref,
|
|
91
|
+
dihedral_ref=dihedral_ref, dihedral_deg=dihedral_deg,
|
|
92
|
+
hybridization=Hybridization.SP2)
|
|
93
|
+
|
|
94
|
+
# C=O (double bond)
|
|
95
|
+
o1 = mol.add_atom_bonded(
|
|
96
|
+
"O", c_idx, bond_order=2, angle_ref=attach_to,
|
|
97
|
+
bond_angle_deg=SP2_ANGLE, dihedral_deg=0.0,
|
|
98
|
+
rotatable=False)
|
|
99
|
+
|
|
100
|
+
# C-OH (single bond)
|
|
101
|
+
o2 = mol.add_atom_bonded(
|
|
102
|
+
"O", c_idx, bond_order=1, angle_ref=attach_to,
|
|
103
|
+
bond_angle_deg=SP2_ANGLE, dihedral_deg=180.0,
|
|
104
|
+
hybridization=Hybridization.SP3)
|
|
105
|
+
|
|
106
|
+
h = mol.add_atom_bonded(
|
|
107
|
+
"H", o2, bond_order=1, angle_ref=c_idx,
|
|
108
|
+
bond_angle_deg=SP3_ANGLE, dihedral_deg=0.0,
|
|
109
|
+
rotatable=False)
|
|
110
|
+
|
|
111
|
+
return {"C": c_idx, "O1": o1, "O2": o2, "H": h}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def add_carbonyl(mol: Molecule, attach_to: int,
|
|
115
|
+
angle_ref: int | None = None,
|
|
116
|
+
dihedral_deg: float = 0.0) -> dict[str, int]:
|
|
117
|
+
"""Attach C=O to an atom. Returns {'O': idx}."""
|
|
118
|
+
if angle_ref is None:
|
|
119
|
+
nbrs = mol.neighbors(attach_to)
|
|
120
|
+
angle_ref = nbrs[0] if nbrs else 0
|
|
121
|
+
|
|
122
|
+
o_idx = mol.add_atom_bonded(
|
|
123
|
+
"O", attach_to, bond_order=2, angle_ref=angle_ref,
|
|
124
|
+
bond_angle_deg=SP2_ANGLE, dihedral_deg=dihedral_deg,
|
|
125
|
+
rotatable=False)
|
|
126
|
+
|
|
127
|
+
return {"O": o_idx}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def add_amide(mol: Molecule, attach_to: int,
|
|
131
|
+
angle_ref: int | None = None,
|
|
132
|
+
dihedral_ref: int | None = None,
|
|
133
|
+
dihedral_deg: float = 180.0) -> dict[str, int]:
|
|
134
|
+
"""Attach -CONH2 to an atom. Returns dict of created indices."""
|
|
135
|
+
if angle_ref is None:
|
|
136
|
+
nbrs = mol.neighbors(attach_to)
|
|
137
|
+
angle_ref = nbrs[0] if nbrs else 0
|
|
138
|
+
|
|
139
|
+
c_idx = mol.add_atom_bonded(
|
|
140
|
+
"C", attach_to, bond_order=1, angle_ref=angle_ref,
|
|
141
|
+
dihedral_ref=dihedral_ref, dihedral_deg=dihedral_deg,
|
|
142
|
+
hybridization=Hybridization.SP2)
|
|
143
|
+
|
|
144
|
+
o_idx = mol.add_atom_bonded(
|
|
145
|
+
"O", c_idx, bond_order=2, angle_ref=attach_to,
|
|
146
|
+
bond_angle_deg=SP2_ANGLE, dihedral_deg=0.0,
|
|
147
|
+
rotatable=False)
|
|
148
|
+
|
|
149
|
+
n_idx = mol.add_atom_bonded(
|
|
150
|
+
"N", c_idx, bond_order=1, angle_ref=attach_to,
|
|
151
|
+
bond_angle_deg=SP2_ANGLE, dihedral_deg=180.0,
|
|
152
|
+
bond_length=AMIDE_CN,
|
|
153
|
+
hybridization=Hybridization.SP2)
|
|
154
|
+
|
|
155
|
+
h1 = mol.add_atom_bonded(
|
|
156
|
+
"H", n_idx, bond_order=1, angle_ref=c_idx,
|
|
157
|
+
bond_angle_deg=SP2_ANGLE, dihedral_deg=0.0,
|
|
158
|
+
rotatable=False)
|
|
159
|
+
|
|
160
|
+
h2 = mol.add_atom_bonded(
|
|
161
|
+
"H", n_idx, bond_order=1, angle_ref=c_idx,
|
|
162
|
+
bond_angle_deg=SP2_ANGLE, dihedral_deg=180.0,
|
|
163
|
+
rotatable=False)
|
|
164
|
+
|
|
165
|
+
return {"C": c_idx, "O": o_idx, "N": n_idx, "H1": h1, "H2": h2}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def add_thiol(mol: Molecule, attach_to: int,
|
|
169
|
+
angle_ref: int | None = None,
|
|
170
|
+
dihedral_ref: int | None = None,
|
|
171
|
+
dihedral_deg: float = 180.0) -> dict[str, int]:
|
|
172
|
+
"""Attach -SH to an atom. Returns {'S': idx, 'H': idx}."""
|
|
173
|
+
if angle_ref is None:
|
|
174
|
+
nbrs = mol.neighbors(attach_to)
|
|
175
|
+
angle_ref = nbrs[0] if nbrs else 0
|
|
176
|
+
|
|
177
|
+
s_idx = mol.add_atom_bonded(
|
|
178
|
+
"S", attach_to, bond_order=1, angle_ref=angle_ref,
|
|
179
|
+
dihedral_ref=dihedral_ref, dihedral_deg=dihedral_deg,
|
|
180
|
+
hybridization=Hybridization.SP3)
|
|
181
|
+
|
|
182
|
+
h_idx = mol.add_atom_bonded(
|
|
183
|
+
"H", s_idx, bond_order=1, angle_ref=attach_to,
|
|
184
|
+
bond_angle_deg=96.0, # H-S-C angle ~96 deg
|
|
185
|
+
dihedral_deg=180.0,
|
|
186
|
+
rotatable=False)
|
|
187
|
+
|
|
188
|
+
return {"S": s_idx, "H": h_idx}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _place_planar_ring(mol: Molecule, attach_to: int,
|
|
192
|
+
symbols: list[str],
|
|
193
|
+
bond_length_val: float = CC_AROMATIC,
|
|
194
|
+
angle_ref: int | None = None,
|
|
195
|
+
dihedral_deg: float = 90.0) -> list[int]:
|
|
196
|
+
"""Place a planar ring of atoms bonded to *attach_to*.
|
|
197
|
+
|
|
198
|
+
The ring is built analytically: vertices of a regular polygon with
|
|
199
|
+
the first atom bonded to attach_to. Ring normal is set perpendicular
|
|
200
|
+
to the attach_to bond direction.
|
|
201
|
+
|
|
202
|
+
Returns a list of atom indices for the ring atoms.
|
|
203
|
+
"""
|
|
204
|
+
n_ring = len(symbols)
|
|
205
|
+
if angle_ref is None:
|
|
206
|
+
nbrs = mol.neighbors(attach_to)
|
|
207
|
+
angle_ref = nbrs[0] if nbrs else 0
|
|
208
|
+
|
|
209
|
+
center_pos = mol.atoms[attach_to].position
|
|
210
|
+
ref_pos = mol.atoms[angle_ref].position
|
|
211
|
+
|
|
212
|
+
# Bond direction from attach_to toward angle_ref
|
|
213
|
+
bond_dir = normalize(center_pos - ref_pos)
|
|
214
|
+
|
|
215
|
+
# Ring normal: perpendicular to bond direction
|
|
216
|
+
up = np.array([0.0, 0.0, 1.0])
|
|
217
|
+
if abs(np.dot(bond_dir, up)) > 0.9:
|
|
218
|
+
up = np.array([0.0, 1.0, 0.0])
|
|
219
|
+
ring_normal = normalize(np.cross(bond_dir, up))
|
|
220
|
+
|
|
221
|
+
# Rotate ring_normal by dihedral_deg around bond_dir
|
|
222
|
+
from molbuilder.core.geometry import rotation_matrix
|
|
223
|
+
R_dih = rotation_matrix(bond_dir, math.radians(dihedral_deg))
|
|
224
|
+
ring_normal = R_dih @ ring_normal
|
|
225
|
+
|
|
226
|
+
# In-plane direction perpendicular to ring normal and along bond
|
|
227
|
+
in_plane = normalize(np.cross(ring_normal, bond_dir))
|
|
228
|
+
|
|
229
|
+
# Ring radius from bond length
|
|
230
|
+
# For a regular n-gon with edge length L: R = L / (2 * sin(pi/n))
|
|
231
|
+
ring_radius = bond_length_val / (2.0 * math.sin(math.pi / n_ring))
|
|
232
|
+
|
|
233
|
+
# Center of ring: along bond_dir from attach_to
|
|
234
|
+
# The first ring atom bonds to attach_to, so the ring center
|
|
235
|
+
# is offset by ring_radius from the first atom position.
|
|
236
|
+
# First ring atom is at distance bond_length from attach_to
|
|
237
|
+
# in the bond_dir direction.
|
|
238
|
+
first_atom_pos = center_pos + bond_dir * bond_length_val
|
|
239
|
+
# Ring center is at first_atom_pos - ring_radius * bond_dir
|
|
240
|
+
# (ring is oriented so first atom is closest to attach_to)
|
|
241
|
+
# Actually, let the first atom be at the "top" of the ring.
|
|
242
|
+
ring_center = first_atom_pos - ring_radius * bond_dir
|
|
243
|
+
|
|
244
|
+
ring_indices = []
|
|
245
|
+
for i in range(n_ring):
|
|
246
|
+
angle = 2.0 * math.pi * i / n_ring
|
|
247
|
+
pos = (ring_center
|
|
248
|
+
+ ring_radius * math.cos(angle) * bond_dir
|
|
249
|
+
+ ring_radius * math.sin(angle) * in_plane)
|
|
250
|
+
hyb = Hybridization.SP2
|
|
251
|
+
idx = mol.add_atom(symbols[i], pos, hyb)
|
|
252
|
+
ring_indices.append(idx)
|
|
253
|
+
|
|
254
|
+
# Bond ring atoms to each other
|
|
255
|
+
for i in range(n_ring):
|
|
256
|
+
j = (i + 1) % n_ring
|
|
257
|
+
if i < n_ring - 1:
|
|
258
|
+
mol.add_bond(ring_indices[i], ring_indices[j],
|
|
259
|
+
order=1, rotatable=False)
|
|
260
|
+
else:
|
|
261
|
+
mol.close_ring(ring_indices[i], ring_indices[j])
|
|
262
|
+
|
|
263
|
+
# Bond first ring atom to attach_to
|
|
264
|
+
mol.add_bond(attach_to, ring_indices[0], order=1, rotatable=True)
|
|
265
|
+
|
|
266
|
+
return ring_indices
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def add_phenyl_ring(mol: Molecule, attach_to: int,
|
|
270
|
+
angle_ref: int | None = None,
|
|
271
|
+
dihedral_deg: float = 90.0) -> dict[str, int]:
|
|
272
|
+
"""Attach a phenyl ring (-C6H5) to an atom.
|
|
273
|
+
|
|
274
|
+
Returns {'ring': [6 carbon indices], 'H': [5 hydrogen indices]}.
|
|
275
|
+
"""
|
|
276
|
+
symbols = ["C"] * 6
|
|
277
|
+
ring = _place_planar_ring(mol, attach_to, symbols,
|
|
278
|
+
bond_length_val=CC_AROMATIC,
|
|
279
|
+
angle_ref=angle_ref,
|
|
280
|
+
dihedral_deg=dihedral_deg)
|
|
281
|
+
|
|
282
|
+
# Add H to each ring C except the one bonded to attach_to (ring[0])
|
|
283
|
+
h_indices = []
|
|
284
|
+
for i in range(1, 6):
|
|
285
|
+
c_pos = mol.atoms[ring[i]].position
|
|
286
|
+
# H points outward from ring center
|
|
287
|
+
ring_center = np.mean(
|
|
288
|
+
[mol.atoms[ring[j]].position for j in range(6)], axis=0)
|
|
289
|
+
outward = normalize(c_pos - ring_center)
|
|
290
|
+
h_pos = c_pos + outward * bond_length("C", "H", 1)
|
|
291
|
+
h_idx = mol.add_atom("H", h_pos)
|
|
292
|
+
mol.add_bond(ring[i], h_idx, order=1, rotatable=False)
|
|
293
|
+
h_indices.append(h_idx)
|
|
294
|
+
|
|
295
|
+
return {"ring": ring, "H": h_indices}
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def add_imidazole_ring(mol: Molecule, attach_to: int,
|
|
299
|
+
angle_ref: int | None = None,
|
|
300
|
+
dihedral_deg: float = 90.0) -> dict[str, int]:
|
|
301
|
+
"""Attach an imidazole ring to an atom (5-membered: C-N=C-NH-C).
|
|
302
|
+
|
|
303
|
+
Numbering: CG-ND1=CE1-NE2(H)-CD2, with CG bonded to attach_to.
|
|
304
|
+
Returns dict with atom name -> index.
|
|
305
|
+
"""
|
|
306
|
+
# Imidazole: 5-membered ring C3N2
|
|
307
|
+
# CG(0)-ND1(1)=CE1(2)-NE2(3)-CD2(4)-CG(close)
|
|
308
|
+
symbols = ["C", "N", "C", "N", "C"]
|
|
309
|
+
ring = _place_planar_ring(mol, attach_to, symbols,
|
|
310
|
+
bond_length_val=CN_AROMATIC,
|
|
311
|
+
angle_ref=angle_ref,
|
|
312
|
+
dihedral_deg=dihedral_deg)
|
|
313
|
+
|
|
314
|
+
ring_center = np.mean(
|
|
315
|
+
[mol.atoms[ring[j]].position for j in range(5)], axis=0)
|
|
316
|
+
|
|
317
|
+
# Add H to CE1 (ring[2]) and CD2 (ring[4])
|
|
318
|
+
h_indices = {}
|
|
319
|
+
for label, i in [("HE1", 2), ("HD2", 4)]:
|
|
320
|
+
c_pos = mol.atoms[ring[i]].position
|
|
321
|
+
outward = normalize(c_pos - ring_center)
|
|
322
|
+
h_pos = c_pos + outward * bond_length("C", "H", 1)
|
|
323
|
+
h_idx = mol.add_atom("H", h_pos)
|
|
324
|
+
mol.add_bond(ring[i], h_idx, order=1, rotatable=False)
|
|
325
|
+
h_indices[label] = h_idx
|
|
326
|
+
|
|
327
|
+
# Add H to NE2 (ring[3]) -- the NH of imidazole
|
|
328
|
+
n_pos = mol.atoms[ring[3]].position
|
|
329
|
+
outward = normalize(n_pos - ring_center)
|
|
330
|
+
h_pos = n_pos + outward * bond_length("N", "H", 1)
|
|
331
|
+
he2 = mol.add_atom("H", h_pos)
|
|
332
|
+
mol.add_bond(ring[3], he2, order=1, rotatable=False)
|
|
333
|
+
h_indices["HE2"] = he2
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
"CG": ring[0], "ND1": ring[1], "CE1": ring[2],
|
|
337
|
+
"NE2": ring[3], "CD2": ring[4], **h_indices,
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def add_indole_ring(mol: Molecule, attach_to: int,
|
|
342
|
+
angle_ref: int | None = None,
|
|
343
|
+
dihedral_deg: float = 90.0) -> dict[str, int]:
|
|
344
|
+
"""Attach an indole ring system (fused 6+5 ring, as in tryptophan).
|
|
345
|
+
|
|
346
|
+
The 5-membered ring is bonded to attach_to via CG.
|
|
347
|
+
Returns dict with atom name -> index.
|
|
348
|
+
"""
|
|
349
|
+
# Build the 6-membered ring (benzene part) first, then the 5-membered
|
|
350
|
+
# ring fused to it sharing two atoms (CD2, CE2).
|
|
351
|
+
# Indole numbering:
|
|
352
|
+
# 5-ring: CG-CD1=NE1-CE2-CD2-CG
|
|
353
|
+
# 6-ring: CE2-CZ2-CH2-CZ3-CE3-CD2
|
|
354
|
+
|
|
355
|
+
# Place 5-membered ring
|
|
356
|
+
symbols_5 = ["C", "C", "N", "C", "C"]
|
|
357
|
+
ring5 = _place_planar_ring(mol, attach_to, symbols_5,
|
|
358
|
+
bond_length_val=CN_AROMATIC,
|
|
359
|
+
angle_ref=angle_ref,
|
|
360
|
+
dihedral_deg=dihedral_deg)
|
|
361
|
+
|
|
362
|
+
# CG=ring5[0], CD1=ring5[1], NE1=ring5[2], CE2=ring5[3], CD2=ring5[4]
|
|
363
|
+
# Now build the 6-membered ring fused at CE2-CD2
|
|
364
|
+
ce2_pos = mol.atoms[ring5[3]].position
|
|
365
|
+
cd2_pos = mol.atoms[ring5[4]].position
|
|
366
|
+
|
|
367
|
+
# Ring center of the 5-ring
|
|
368
|
+
ring5_center = np.mean(
|
|
369
|
+
[mol.atoms[ring5[j]].position for j in range(5)], axis=0)
|
|
370
|
+
|
|
371
|
+
# The 6-ring extends outward from the CE2-CD2 edge
|
|
372
|
+
midpoint = (ce2_pos + cd2_pos) / 2.0
|
|
373
|
+
outward = normalize(midpoint - ring5_center)
|
|
374
|
+
|
|
375
|
+
# Build 4 new atoms for the 6-ring (CZ2, CH2, CZ3, CE3)
|
|
376
|
+
# The edge CE2-CD2 is shared; we need 4 more vertices to close a hexagon.
|
|
377
|
+
edge_vec = normalize(ce2_pos - cd2_pos)
|
|
378
|
+
edge_len = float(np.linalg.norm(ce2_pos - cd2_pos))
|
|
379
|
+
|
|
380
|
+
# Use the ring normal from the 5-ring
|
|
381
|
+
ring_normal = normalize(np.cross(
|
|
382
|
+
mol.atoms[ring5[1]].position - mol.atoms[ring5[0]].position,
|
|
383
|
+
mol.atoms[ring5[4]].position - mol.atoms[ring5[0]].position,
|
|
384
|
+
))
|
|
385
|
+
|
|
386
|
+
# For a regular hexagon with edge length = CC_AROMATIC:
|
|
387
|
+
h_offset = CC_AROMATIC * math.sin(math.radians(60)) # height
|
|
388
|
+
half_edge = CC_AROMATIC * math.cos(math.radians(60)) # 0.5 * edge
|
|
389
|
+
|
|
390
|
+
# 4 new atoms arranged as a hexagon extension
|
|
391
|
+
cz2_pos = ce2_pos + outward * h_offset + edge_vec * half_edge
|
|
392
|
+
ce3_pos = cd2_pos + outward * h_offset - edge_vec * half_edge
|
|
393
|
+
ch2_pos = cz2_pos + outward * h_offset - edge_vec * half_edge
|
|
394
|
+
cz3_pos = ce3_pos + outward * h_offset + edge_vec * half_edge
|
|
395
|
+
|
|
396
|
+
cz2 = mol.add_atom("C", cz2_pos, Hybridization.SP2)
|
|
397
|
+
ch2 = mol.add_atom("C", ch2_pos, Hybridization.SP2)
|
|
398
|
+
cz3 = mol.add_atom("C", cz3_pos, Hybridization.SP2)
|
|
399
|
+
ce3 = mol.add_atom("C", ce3_pos, Hybridization.SP2)
|
|
400
|
+
|
|
401
|
+
# Bonds for the 6-ring: CE2-CZ2-CH2-CZ3-CE3-CD2
|
|
402
|
+
mol.add_bond(ring5[3], cz2, order=1, rotatable=False) # CE2-CZ2
|
|
403
|
+
mol.add_bond(cz2, ch2, order=1, rotatable=False) # CZ2-CH2
|
|
404
|
+
mol.add_bond(ch2, cz3, order=1, rotatable=False) # CH2-CZ3
|
|
405
|
+
mol.add_bond(cz3, ce3, order=1, rotatable=False) # CZ3-CE3
|
|
406
|
+
mol.close_ring(ce3, ring5[4]) # CE3-CD2
|
|
407
|
+
|
|
408
|
+
# Add H to exposed atoms
|
|
409
|
+
all_ring = ring5 + [cz2, ch2, cz3, ce3]
|
|
410
|
+
overall_center = np.mean(
|
|
411
|
+
[mol.atoms[idx].position for idx in all_ring], axis=0)
|
|
412
|
+
|
|
413
|
+
h_dict = {}
|
|
414
|
+
# CD1 (ring5[1]), NE1 gets H, CZ2, CH2, CZ3, CE3 get H
|
|
415
|
+
for label, idx in [("HD1", ring5[1]), ("HZ2", cz2),
|
|
416
|
+
("HH2", ch2), ("HZ3", cz3), ("HE3", ce3)]:
|
|
417
|
+
a_pos = mol.atoms[idx].position
|
|
418
|
+
out = normalize(a_pos - overall_center)
|
|
419
|
+
h_pos = a_pos + out * bond_length("C", "H", 1)
|
|
420
|
+
h_i = mol.add_atom("H", h_pos)
|
|
421
|
+
mol.add_bond(idx, h_i, order=1, rotatable=False)
|
|
422
|
+
h_dict[label] = h_i
|
|
423
|
+
|
|
424
|
+
# NE1 hydrogen
|
|
425
|
+
ne1_pos = mol.atoms[ring5[2]].position
|
|
426
|
+
out = normalize(ne1_pos - overall_center)
|
|
427
|
+
h_pos = ne1_pos + out * bond_length("N", "H", 1)
|
|
428
|
+
he1 = mol.add_atom("H", h_pos)
|
|
429
|
+
mol.add_bond(ring5[2], he1, order=1, rotatable=False)
|
|
430
|
+
h_dict["HE1"] = he1
|
|
431
|
+
|
|
432
|
+
return {
|
|
433
|
+
"CG": ring5[0], "CD1": ring5[1], "NE1": ring5[2],
|
|
434
|
+
"CE2": ring5[3], "CD2": ring5[4],
|
|
435
|
+
"CZ2": cz2, "CH2": ch2, "CZ3": cz3, "CE3": ce3,
|
|
436
|
+
**h_dict,
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def add_guanidinium(mol: Molecule, attach_to: int,
|
|
441
|
+
angle_ref: int | None = None,
|
|
442
|
+
dihedral_ref: int | None = None,
|
|
443
|
+
dihedral_deg: float = 180.0) -> dict[str, int]:
|
|
444
|
+
"""Attach a guanidinium group -C(=NH)(NH2)(NH2) to an atom.
|
|
445
|
+
|
|
446
|
+
Returns dict with atom name -> index.
|
|
447
|
+
"""
|
|
448
|
+
if angle_ref is None:
|
|
449
|
+
nbrs = mol.neighbors(attach_to)
|
|
450
|
+
angle_ref = nbrs[0] if nbrs else 0
|
|
451
|
+
|
|
452
|
+
cz = mol.add_atom_bonded(
|
|
453
|
+
"C", attach_to, bond_order=1, angle_ref=angle_ref,
|
|
454
|
+
dihedral_ref=dihedral_ref, dihedral_deg=dihedral_deg,
|
|
455
|
+
hybridization=Hybridization.SP2)
|
|
456
|
+
|
|
457
|
+
# NH1 (=NH)
|
|
458
|
+
nh1 = mol.add_atom_bonded(
|
|
459
|
+
"N", cz, bond_order=2, angle_ref=attach_to,
|
|
460
|
+
bond_angle_deg=SP2_ANGLE, dihedral_deg=0.0,
|
|
461
|
+
bond_length=1.33, hybridization=Hybridization.SP2)
|
|
462
|
+
hh11 = mol.add_atom_bonded(
|
|
463
|
+
"H", nh1, bond_order=1, angle_ref=cz,
|
|
464
|
+
bond_angle_deg=SP2_ANGLE, dihedral_deg=180.0,
|
|
465
|
+
rotatable=False)
|
|
466
|
+
|
|
467
|
+
# NH2 (-NH2)
|
|
468
|
+
nh2 = mol.add_atom_bonded(
|
|
469
|
+
"N", cz, bond_order=1, angle_ref=attach_to,
|
|
470
|
+
bond_angle_deg=SP2_ANGLE, dihedral_deg=180.0,
|
|
471
|
+
bond_length=AMIDE_CN, hybridization=Hybridization.SP2)
|
|
472
|
+
hh21 = mol.add_atom_bonded(
|
|
473
|
+
"H", nh2, bond_order=1, angle_ref=cz,
|
|
474
|
+
bond_angle_deg=SP2_ANGLE, dihedral_deg=0.0,
|
|
475
|
+
rotatable=False)
|
|
476
|
+
hh22 = mol.add_atom_bonded(
|
|
477
|
+
"H", nh2, bond_order=1, angle_ref=cz,
|
|
478
|
+
bond_angle_deg=SP2_ANGLE, dihedral_deg=180.0,
|
|
479
|
+
rotatable=False)
|
|
480
|
+
|
|
481
|
+
return {
|
|
482
|
+
"CZ": cz, "NH1": nh1, "HH11": hh11,
|
|
483
|
+
"NH2": nh2, "HH21": hh21, "HH22": hh22,
|
|
484
|
+
}
|