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.
Files changed (78) hide show
  1. molbuilder/__init__.py +8 -0
  2. molbuilder/__main__.py +6 -0
  3. molbuilder/atomic/__init__.py +4 -0
  4. molbuilder/atomic/bohr.py +235 -0
  5. molbuilder/atomic/quantum_atom.py +334 -0
  6. molbuilder/atomic/quantum_numbers.py +196 -0
  7. molbuilder/atomic/wavefunctions.py +297 -0
  8. molbuilder/bonding/__init__.py +4 -0
  9. molbuilder/bonding/covalent.py +442 -0
  10. molbuilder/bonding/lewis.py +347 -0
  11. molbuilder/bonding/vsepr.py +433 -0
  12. molbuilder/cli/__init__.py +1 -0
  13. molbuilder/cli/demos.py +516 -0
  14. molbuilder/cli/menu.py +127 -0
  15. molbuilder/cli/wizard.py +831 -0
  16. molbuilder/core/__init__.py +6 -0
  17. molbuilder/core/bond_data.py +170 -0
  18. molbuilder/core/constants.py +51 -0
  19. molbuilder/core/element_properties.py +183 -0
  20. molbuilder/core/elements.py +181 -0
  21. molbuilder/core/geometry.py +232 -0
  22. molbuilder/gui/__init__.py +2 -0
  23. molbuilder/gui/app.py +286 -0
  24. molbuilder/gui/canvas3d.py +115 -0
  25. molbuilder/gui/dialogs.py +117 -0
  26. molbuilder/gui/event_handler.py +118 -0
  27. molbuilder/gui/sidebar.py +105 -0
  28. molbuilder/gui/toolbar.py +71 -0
  29. molbuilder/io/__init__.py +1 -0
  30. molbuilder/io/json_io.py +146 -0
  31. molbuilder/io/mol_sdf.py +169 -0
  32. molbuilder/io/pdb.py +184 -0
  33. molbuilder/io/smiles_io.py +47 -0
  34. molbuilder/io/xyz.py +103 -0
  35. molbuilder/molecule/__init__.py +2 -0
  36. molbuilder/molecule/amino_acids.py +919 -0
  37. molbuilder/molecule/builders.py +257 -0
  38. molbuilder/molecule/conformations.py +70 -0
  39. molbuilder/molecule/functional_groups.py +484 -0
  40. molbuilder/molecule/graph.py +712 -0
  41. molbuilder/molecule/peptides.py +13 -0
  42. molbuilder/molecule/stereochemistry.py +6 -0
  43. molbuilder/process/__init__.py +3 -0
  44. molbuilder/process/conditions.py +260 -0
  45. molbuilder/process/costing.py +316 -0
  46. molbuilder/process/purification.py +285 -0
  47. molbuilder/process/reactor.py +297 -0
  48. molbuilder/process/safety.py +476 -0
  49. molbuilder/process/scale_up.py +427 -0
  50. molbuilder/process/solvent_systems.py +204 -0
  51. molbuilder/reactions/__init__.py +3 -0
  52. molbuilder/reactions/functional_group_detect.py +728 -0
  53. molbuilder/reactions/knowledge_base.py +1716 -0
  54. molbuilder/reactions/reaction_types.py +102 -0
  55. molbuilder/reactions/reagent_data.py +1248 -0
  56. molbuilder/reactions/retrosynthesis.py +1430 -0
  57. molbuilder/reactions/synthesis_route.py +377 -0
  58. molbuilder/reports/__init__.py +158 -0
  59. molbuilder/reports/cost_report.py +206 -0
  60. molbuilder/reports/molecule_report.py +279 -0
  61. molbuilder/reports/safety_report.py +296 -0
  62. molbuilder/reports/synthesis_report.py +283 -0
  63. molbuilder/reports/text_formatter.py +170 -0
  64. molbuilder/smiles/__init__.py +4 -0
  65. molbuilder/smiles/parser.py +487 -0
  66. molbuilder/smiles/tokenizer.py +291 -0
  67. molbuilder/smiles/writer.py +375 -0
  68. molbuilder/visualization/__init__.py +1 -0
  69. molbuilder/visualization/bohr_viz.py +166 -0
  70. molbuilder/visualization/molecule_viz.py +368 -0
  71. molbuilder/visualization/quantum_viz.py +434 -0
  72. molbuilder/visualization/theme.py +12 -0
  73. molbuilder-1.0.0.dist-info/METADATA +360 -0
  74. molbuilder-1.0.0.dist-info/RECORD +78 -0
  75. molbuilder-1.0.0.dist-info/WHEEL +5 -0
  76. molbuilder-1.0.0.dist-info/entry_points.txt +2 -0
  77. molbuilder-1.0.0.dist-info/licenses/LICENSE +21 -0
  78. 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