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,257 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Molecule builder functions.
|
|
3
|
+
|
|
4
|
+
Convenience functions that construct common molecules with correct 3D
|
|
5
|
+
geometry: ethane, butane, cyclohexane, 2-butene, and a generic chiral
|
|
6
|
+
centre.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import math
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
from molbuilder.molecule.graph import Molecule, Hybridization, RingConformation
|
|
16
|
+
from molbuilder.core.bond_data import bond_length, SP3_ANGLE, SP2_ANGLE
|
|
17
|
+
from molbuilder.core.geometry import normalize, available_tetrahedral_dirs, add_sp3_hydrogens
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def build_ethane(dihedral_deg: float = 60.0) -> Molecule:
|
|
21
|
+
"""Build ethane (C2H6) with a specified H-C-C-H dihedral.
|
|
22
|
+
|
|
23
|
+
60 deg = staggered (default, lowest energy).
|
|
24
|
+
0 deg = eclipsed (highest energy).
|
|
25
|
+
"""
|
|
26
|
+
mol = Molecule("ethane")
|
|
27
|
+
CC = bond_length("C", "C", 1)
|
|
28
|
+
CH = bond_length("C", "H", 1)
|
|
29
|
+
|
|
30
|
+
c0 = mol.add_atom("C", [0.0, 0.0, 0.0], Hybridization.SP3)
|
|
31
|
+
c1 = mol.add_atom("C", [0.0, 0.0, CC], Hybridization.SP3)
|
|
32
|
+
mol.add_bond(c0, c1)
|
|
33
|
+
|
|
34
|
+
# Place 3 H on C0 at tetrahedral positions pointing away from C1
|
|
35
|
+
c0_bond_dir = normalize(mol.atoms[c1].position - mol.atoms[c0].position)
|
|
36
|
+
c0_h_dirs = available_tetrahedral_dirs([c0_bond_dir], 3)
|
|
37
|
+
for d in c0_h_dirs:
|
|
38
|
+
h_pos = mol.atoms[c0].position + CH * d
|
|
39
|
+
h_idx = mol.add_atom("H", h_pos)
|
|
40
|
+
mol.add_bond(c0, h_idx, order=1, rotatable=False)
|
|
41
|
+
|
|
42
|
+
# Place 3 H on C1 at tetrahedral positions pointing away from C0,
|
|
43
|
+
# then rotate by dihedral_deg
|
|
44
|
+
c1_bond_dir = normalize(mol.atoms[c0].position - mol.atoms[c1].position)
|
|
45
|
+
c1_h_dirs = available_tetrahedral_dirs([c1_bond_dir], 3)
|
|
46
|
+
for d in c1_h_dirs:
|
|
47
|
+
h_pos = mol.atoms[c1].position + CH * d
|
|
48
|
+
h_idx = mol.add_atom("H", h_pos)
|
|
49
|
+
mol.add_bond(c1, h_idx, order=1, rotatable=False)
|
|
50
|
+
|
|
51
|
+
# Set the desired dihedral angle between the first H on each carbon
|
|
52
|
+
h_on_c0 = [i for i in mol.neighbors(c0) if mol.atoms[i].symbol == "H"]
|
|
53
|
+
h_on_c1 = [i for i in mol.neighbors(c1) if mol.atoms[i].symbol == "H"]
|
|
54
|
+
if h_on_c0 and h_on_c1:
|
|
55
|
+
mol.set_dihedral(h_on_c0[0], c0, c1, h_on_c1[0], dihedral_deg)
|
|
56
|
+
|
|
57
|
+
if abs(dihedral_deg - 60.0) < 1:
|
|
58
|
+
label = "staggered"
|
|
59
|
+
elif abs(dihedral_deg) < 1 or abs(dihedral_deg - 360) < 1:
|
|
60
|
+
label = "eclipsed"
|
|
61
|
+
else:
|
|
62
|
+
label = f"dihedral={dihedral_deg:.0f}"
|
|
63
|
+
mol.name = f"ethane ({label})"
|
|
64
|
+
return mol
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def build_butane(central_dihedral_deg: float = 180.0) -> Molecule:
|
|
68
|
+
"""Build n-butane (C4H10) with a specified C-C-C-C dihedral.
|
|
69
|
+
|
|
70
|
+
180 = anti (default, lowest energy).
|
|
71
|
+
60 = gauche.
|
|
72
|
+
0 = eclipsed (syn-periplanar).
|
|
73
|
+
120 = eclipsed (anti-clinal).
|
|
74
|
+
"""
|
|
75
|
+
mol = Molecule("butane")
|
|
76
|
+
CC = bond_length("C", "C", 1)
|
|
77
|
+
|
|
78
|
+
# Carbon backbone
|
|
79
|
+
c0 = mol.add_atom("C", [0.0, 0.0, 0.0], Hybridization.SP3)
|
|
80
|
+
c1 = mol.add_atom_bonded("C", c0, bond_length=CC,
|
|
81
|
+
hybridization=Hybridization.SP3)
|
|
82
|
+
c2 = mol.add_atom_bonded("C", c1, angle_ref=c0,
|
|
83
|
+
bond_angle_deg=SP3_ANGLE,
|
|
84
|
+
dihedral_deg=180.0,
|
|
85
|
+
hybridization=Hybridization.SP3)
|
|
86
|
+
c3 = mol.add_atom_bonded("C", c2, angle_ref=c1,
|
|
87
|
+
dihedral_ref=c0,
|
|
88
|
+
bond_angle_deg=SP3_ANGLE,
|
|
89
|
+
dihedral_deg=central_dihedral_deg,
|
|
90
|
+
hybridization=Hybridization.SP3)
|
|
91
|
+
|
|
92
|
+
# Hydrogens: C0 gets 3, C1 gets 2, C2 gets 2, C3 gets 3
|
|
93
|
+
add_sp3_hydrogens(mol, c0, 3)
|
|
94
|
+
add_sp3_hydrogens(mol, c1, 2)
|
|
95
|
+
add_sp3_hydrogens(mol, c2, 2)
|
|
96
|
+
add_sp3_hydrogens(mol, c3, 3)
|
|
97
|
+
|
|
98
|
+
if abs(central_dihedral_deg - 180) < 1:
|
|
99
|
+
label = "anti"
|
|
100
|
+
elif abs(abs(central_dihedral_deg) - 60) < 1:
|
|
101
|
+
label = "gauche"
|
|
102
|
+
elif abs(central_dihedral_deg) < 1:
|
|
103
|
+
label = "eclipsed (syn)"
|
|
104
|
+
elif abs(central_dihedral_deg - 120) < 1:
|
|
105
|
+
label = "eclipsed (anti-clinal)"
|
|
106
|
+
else:
|
|
107
|
+
label = f"dihedral={central_dihedral_deg:.0f}"
|
|
108
|
+
mol.name = f"butane ({label})"
|
|
109
|
+
return mol
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def build_cyclohexane(
|
|
113
|
+
conformation: RingConformation = RingConformation.CHAIR,
|
|
114
|
+
) -> Molecule:
|
|
115
|
+
"""Build cyclohexane (C6H12) in chair or boat conformation.
|
|
116
|
+
|
|
117
|
+
Chair: alternating +/- puckering from crystallographic geometry.
|
|
118
|
+
Boat: atoms 0 and 3 displaced to the same side of the ring plane.
|
|
119
|
+
"""
|
|
120
|
+
mol = Molecule(f"cyclohexane ({conformation.name.lower()})")
|
|
121
|
+
CC = bond_length("C", "C", 1)
|
|
122
|
+
CH = bond_length("C", "H", 1)
|
|
123
|
+
|
|
124
|
+
# Ring radius so that adjacent C-C distance = CC
|
|
125
|
+
# Adjacent carbons are separated by 60 deg in angle and 2*d in z:
|
|
126
|
+
# CC^2 = r^2 + (2d)^2 => r = sqrt(CC^2 - 4*d^2)
|
|
127
|
+
d = 0.253 # puckering amplitude (Angstroms)
|
|
128
|
+
r = math.sqrt(CC ** 2 - (2.0 * d) ** 2)
|
|
129
|
+
|
|
130
|
+
if conformation == RingConformation.CHAIR:
|
|
131
|
+
puckering = [d, -d, d, -d, d, -d]
|
|
132
|
+
elif conformation == RingConformation.BOAT:
|
|
133
|
+
puckering = [d, -d, 0, d, -d, 0]
|
|
134
|
+
else:
|
|
135
|
+
raise ValueError(
|
|
136
|
+
f"Unsupported conformation: {conformation}. "
|
|
137
|
+
f"Use CHAIR or BOAT.")
|
|
138
|
+
|
|
139
|
+
# Place ring carbons
|
|
140
|
+
for i in range(6):
|
|
141
|
+
angle = math.radians(60.0 * i)
|
|
142
|
+
x = r * math.cos(angle)
|
|
143
|
+
y = r * math.sin(angle)
|
|
144
|
+
z = puckering[i]
|
|
145
|
+
mol.add_atom("C", [x, y, z], Hybridization.SP3)
|
|
146
|
+
|
|
147
|
+
# Close ring bonds (non-rotatable)
|
|
148
|
+
for i in range(6):
|
|
149
|
+
mol.add_bond(i, (i + 1) % 6, order=1, rotatable=False)
|
|
150
|
+
|
|
151
|
+
# Add axial and equatorial hydrogens
|
|
152
|
+
center = np.mean([mol.atoms[i].position for i in range(6)], axis=0)
|
|
153
|
+
up = np.array([0.0, 0.0, 1.0])
|
|
154
|
+
|
|
155
|
+
for c_idx in range(6):
|
|
156
|
+
c_pos = mol.atoms[c_idx].position
|
|
157
|
+
radial = normalize(c_pos - center)
|
|
158
|
+
|
|
159
|
+
# Axial direction alternates with puckering
|
|
160
|
+
if puckering[c_idx] > 0:
|
|
161
|
+
ax_dir = up
|
|
162
|
+
else:
|
|
163
|
+
ax_dir = -up
|
|
164
|
+
|
|
165
|
+
eq_dir = normalize(radial + ax_dir * 0.2)
|
|
166
|
+
|
|
167
|
+
# Axial hydrogen
|
|
168
|
+
h_ax_pos = c_pos + CH * normalize(ax_dir)
|
|
169
|
+
h_ax = mol.add_atom("H", h_ax_pos)
|
|
170
|
+
mol.add_bond(c_idx, h_ax, order=1, rotatable=False)
|
|
171
|
+
|
|
172
|
+
# Equatorial hydrogen
|
|
173
|
+
h_eq_pos = c_pos + CH * normalize(eq_dir)
|
|
174
|
+
h_eq = mol.add_atom("H", h_eq_pos)
|
|
175
|
+
mol.add_bond(c_idx, h_eq, order=1, rotatable=False)
|
|
176
|
+
|
|
177
|
+
return mol
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def build_2_butene(is_cis: bool = True) -> Molecule:
|
|
181
|
+
"""Build 2-butene (CH3-CH=CH-CH3) as cis (Z) or trans (E)."""
|
|
182
|
+
label = "Z/cis" if is_cis else "E/trans"
|
|
183
|
+
mol = Molecule(f"2-butene ({label})")
|
|
184
|
+
CC_s = bond_length("C", "C", 1)
|
|
185
|
+
CC_d = bond_length("C", "C", 2)
|
|
186
|
+
|
|
187
|
+
# C0(sp3) - C1(sp2) = C2(sp2) - C3(sp3)
|
|
188
|
+
c0 = mol.add_atom("C", [0.0, 0.0, 0.0], Hybridization.SP3)
|
|
189
|
+
c1 = mol.add_atom_bonded("C", c0, bond_length=CC_s,
|
|
190
|
+
hybridization=Hybridization.SP2)
|
|
191
|
+
c2 = mol.add_atom_bonded("C", c1, bond_order=2,
|
|
192
|
+
angle_ref=c0,
|
|
193
|
+
bond_length=CC_d,
|
|
194
|
+
bond_angle_deg=SP2_ANGLE,
|
|
195
|
+
dihedral_deg=180.0,
|
|
196
|
+
hybridization=Hybridization.SP2)
|
|
197
|
+
|
|
198
|
+
# cis: C0 and C3 on same side (dihedral C0-C1-C2-C3 = 0)
|
|
199
|
+
# trans: opposite sides (dihedral = 180)
|
|
200
|
+
dih = 0.0 if is_cis else 180.0
|
|
201
|
+
c3 = mol.add_atom_bonded("C", c2, angle_ref=c1,
|
|
202
|
+
dihedral_ref=c0,
|
|
203
|
+
bond_length=CC_s,
|
|
204
|
+
bond_angle_deg=SP2_ANGLE,
|
|
205
|
+
dihedral_deg=dih,
|
|
206
|
+
hybridization=Hybridization.SP3)
|
|
207
|
+
|
|
208
|
+
# Vinyl H on C1 and C2
|
|
209
|
+
mol.add_atom_bonded("H", c1, angle_ref=c0,
|
|
210
|
+
bond_angle_deg=SP2_ANGLE,
|
|
211
|
+
dihedral_deg=180.0,
|
|
212
|
+
rotatable=False)
|
|
213
|
+
mol.add_atom_bonded("H", c2, angle_ref=c1,
|
|
214
|
+
bond_angle_deg=SP2_ANGLE,
|
|
215
|
+
dihedral_deg=180.0,
|
|
216
|
+
rotatable=False)
|
|
217
|
+
|
|
218
|
+
# Methyl H's
|
|
219
|
+
add_sp3_hydrogens(mol, c0, 3)
|
|
220
|
+
add_sp3_hydrogens(mol, c3, 3)
|
|
221
|
+
|
|
222
|
+
return mol
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def build_chiral_molecule(
|
|
226
|
+
substituents: list[str] | None = None,
|
|
227
|
+
) -> Molecule:
|
|
228
|
+
"""Build a tetrahedral carbon with four different substituents.
|
|
229
|
+
|
|
230
|
+
Default: CHFClBr (bromochlorofluoromethane).
|
|
231
|
+
"""
|
|
232
|
+
if substituents is None:
|
|
233
|
+
substituents = ["H", "F", "Cl", "Br"]
|
|
234
|
+
|
|
235
|
+
if len(substituents) != 4:
|
|
236
|
+
raise ValueError("Exactly 4 substituents required")
|
|
237
|
+
if len(set(substituents)) != 4:
|
|
238
|
+
raise ValueError("All 4 substituents must be different for chirality")
|
|
239
|
+
|
|
240
|
+
mol = Molecule(f"C({''.join(substituents)}) -- chiral centre")
|
|
241
|
+
|
|
242
|
+
c = mol.add_atom("C", [0.0, 0.0, 0.0], Hybridization.SP3)
|
|
243
|
+
|
|
244
|
+
tet_dirs = [
|
|
245
|
+
np.array([1.0, 1.0, 1.0]) / math.sqrt(3),
|
|
246
|
+
np.array([1.0, -1.0, -1.0]) / math.sqrt(3),
|
|
247
|
+
np.array([-1.0, 1.0, -1.0]) / math.sqrt(3),
|
|
248
|
+
np.array([-1.0, -1.0, 1.0]) / math.sqrt(3),
|
|
249
|
+
]
|
|
250
|
+
|
|
251
|
+
for i, sym in enumerate(substituents):
|
|
252
|
+
bl = bond_length("C", sym, 1)
|
|
253
|
+
pos = tet_dirs[i] * bl
|
|
254
|
+
idx = mol.add_atom(sym, pos)
|
|
255
|
+
mol.add_bond(c, idx, order=1)
|
|
256
|
+
|
|
257
|
+
return mol
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Conformational analysis utilities.
|
|
3
|
+
|
|
4
|
+
Provides classification of dihedral angles into named conformations
|
|
5
|
+
and torsional energy scanning.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import math
|
|
11
|
+
|
|
12
|
+
from molbuilder.molecule.graph import Molecule, TorsionAngle, ConformationType
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def classify_conformation(dihedral_deg: float) -> ConformationType:
|
|
16
|
+
"""Classify a dihedral angle into a named conformation.
|
|
17
|
+
|
|
18
|
+
Ranges (normalised to -180..180):
|
|
19
|
+
|d| < 10 -> ECLIPSED (syn-periplanar)
|
|
20
|
+
|d| ~ 60 -> GAUCHE
|
|
21
|
+
|d| ~ 120 -> ECLIPSED (anti-clinal)
|
|
22
|
+
|d| > 170 -> ANTI (antiperiplanar)
|
|
23
|
+
otherwise -> CUSTOM
|
|
24
|
+
"""
|
|
25
|
+
d = dihedral_deg % 360
|
|
26
|
+
if d > 180:
|
|
27
|
+
d -= 360
|
|
28
|
+
|
|
29
|
+
if abs(d) < 10:
|
|
30
|
+
return ConformationType.ECLIPSED
|
|
31
|
+
if abs(abs(d) - 60) < 15:
|
|
32
|
+
return ConformationType.GAUCHE
|
|
33
|
+
if abs(abs(d) - 180) < 15:
|
|
34
|
+
return ConformationType.ANTI
|
|
35
|
+
if abs(abs(d) - 120) < 15:
|
|
36
|
+
return ConformationType.ECLIPSED
|
|
37
|
+
return ConformationType.CUSTOM
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def scan_torsion(mol: Molecule, j: int, k: int,
|
|
41
|
+
ref_i: int, ref_l: int,
|
|
42
|
+
steps: int = 36) -> list[tuple[float, float]]:
|
|
43
|
+
"""Scan torsional energy as a function of dihedral angle.
|
|
44
|
+
|
|
45
|
+
Rotates the k-side of bond j-k through 360 degrees, computing
|
|
46
|
+
strain at each step. Restores original geometry between each
|
|
47
|
+
step to avoid cumulative numerical drift.
|
|
48
|
+
|
|
49
|
+
Returns list of (angle_deg, energy_kJ_per_mol).
|
|
50
|
+
"""
|
|
51
|
+
originals = [a.position.copy() for a in mol.atoms]
|
|
52
|
+
|
|
53
|
+
results: list[tuple[float, float]] = []
|
|
54
|
+
step_size = 360.0 / steps
|
|
55
|
+
|
|
56
|
+
for s in range(steps):
|
|
57
|
+
# Reset to original before each step
|
|
58
|
+
for i, pos in enumerate(originals):
|
|
59
|
+
mol.atoms[i].position = pos.copy()
|
|
60
|
+
|
|
61
|
+
target = -180.0 + s * step_size
|
|
62
|
+
mol.set_dihedral(ref_i, j, k, ref_l, target)
|
|
63
|
+
energy = mol.torsional_energy(j, k)
|
|
64
|
+
results.append((target, energy.total_kj_per_mol))
|
|
65
|
+
|
|
66
|
+
# Restore original
|
|
67
|
+
for i, pos in enumerate(originals):
|
|
68
|
+
mol.atoms[i].position = pos
|
|
69
|
+
|
|
70
|
+
return results
|