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
molbuilder/cli/wizard.py
ADDED
|
@@ -0,0 +1,831 @@
|
|
|
1
|
+
"""Interactive step-by-step molecule building wizard.
|
|
2
|
+
|
|
3
|
+
Provides a guided CLI workflow for constructing molecules via five
|
|
4
|
+
different approaches: SMILES input, molecular formula (VSEPR), atom-by-
|
|
5
|
+
atom assembly, preset molecules, and peptide/amino-acid sequences.
|
|
6
|
+
|
|
7
|
+
All output is ASCII-safe (Windows cp1252 compatible).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# ===================================================================
|
|
14
|
+
# Amino acid lookup tables
|
|
15
|
+
# ===================================================================
|
|
16
|
+
|
|
17
|
+
# One-letter code -> three-letter code
|
|
18
|
+
_ONE_TO_THREE = {
|
|
19
|
+
"G": "GLY", "A": "ALA", "V": "VAL", "L": "LEU", "I": "ILE",
|
|
20
|
+
"P": "PRO", "F": "PHE", "W": "TRP", "M": "MET", "S": "SER",
|
|
21
|
+
"T": "THR", "C": "CYS", "Y": "TYR", "N": "ASN", "Q": "GLN",
|
|
22
|
+
"D": "ASP", "E": "GLU", "K": "LYS", "R": "ARG", "H": "HIS",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# Full name -> three-letter code
|
|
26
|
+
_NAME_TO_THREE = {
|
|
27
|
+
"GLYCINE": "GLY", "ALANINE": "ALA", "VALINE": "VAL",
|
|
28
|
+
"LEUCINE": "LEU", "ISOLEUCINE": "ILE", "PROLINE": "PRO",
|
|
29
|
+
"PHENYLALANINE": "PHE", "TRYPTOPHAN": "TRP", "METHIONINE": "MET",
|
|
30
|
+
"SERINE": "SER", "THREONINE": "THR", "CYSTEINE": "CYS",
|
|
31
|
+
"TYROSINE": "TYR", "ASPARAGINE": "ASN", "GLUTAMINE": "GLN",
|
|
32
|
+
"ASPARTATE": "ASP", "GLUTAMATE": "GLU", "LYSINE": "LYS",
|
|
33
|
+
"ARGININE": "ARG", "HISTIDINE": "HIS",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ===================================================================
|
|
38
|
+
# Helper utilities
|
|
39
|
+
# ===================================================================
|
|
40
|
+
|
|
41
|
+
def _prompt(message: str, default: str = "") -> str:
|
|
42
|
+
"""Prompt the user for input. Returns stripped text."""
|
|
43
|
+
try:
|
|
44
|
+
val = input(message).strip()
|
|
45
|
+
except (EOFError, KeyboardInterrupt):
|
|
46
|
+
print()
|
|
47
|
+
return default
|
|
48
|
+
return val if val else default
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _prompt_int(message: str, lo: int, hi: int) -> int | None:
|
|
52
|
+
"""Prompt for an integer in [lo, hi]. Returns None on bad input."""
|
|
53
|
+
raw = _prompt(message)
|
|
54
|
+
try:
|
|
55
|
+
val = int(raw)
|
|
56
|
+
if lo <= val <= hi:
|
|
57
|
+
return val
|
|
58
|
+
print(f" Please enter a number between {lo} and {hi}.")
|
|
59
|
+
except ValueError:
|
|
60
|
+
if raw:
|
|
61
|
+
print(f" Invalid number: {raw}")
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _molecular_formula(mol) -> str:
|
|
66
|
+
"""Compute a simple molecular formula string from a Molecule."""
|
|
67
|
+
from collections import Counter
|
|
68
|
+
counts = Counter(atom.symbol for atom in mol.atoms)
|
|
69
|
+
# Hill system: C first, H second, then alphabetical
|
|
70
|
+
parts = []
|
|
71
|
+
for sym in ("C", "H"):
|
|
72
|
+
if sym in counts:
|
|
73
|
+
parts.append(sym if counts[sym] == 1 else f"{sym}{counts[sym]}")
|
|
74
|
+
del counts[sym]
|
|
75
|
+
for sym in sorted(counts):
|
|
76
|
+
parts.append(sym if counts[sym] == 1 else f"{sym}{counts[sym]}")
|
|
77
|
+
return "".join(parts)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _molecular_weight(mol) -> float:
|
|
81
|
+
"""Compute the molecular weight in g/mol."""
|
|
82
|
+
from molbuilder.core.elements import SYMBOL_TO_Z, ELEMENTS
|
|
83
|
+
total = 0.0
|
|
84
|
+
for atom in mol.atoms:
|
|
85
|
+
z = SYMBOL_TO_Z.get(atom.symbol)
|
|
86
|
+
if z is not None:
|
|
87
|
+
total += ELEMENTS[z][2]
|
|
88
|
+
return total
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _validate_element(symbol: str) -> str | None:
|
|
92
|
+
"""Validate and normalise an element symbol. Returns None if invalid."""
|
|
93
|
+
from molbuilder.core.elements import SYMBOL_TO_Z
|
|
94
|
+
sym = symbol.strip()
|
|
95
|
+
if not sym:
|
|
96
|
+
return None
|
|
97
|
+
if len(sym) == 1:
|
|
98
|
+
sym = sym.upper()
|
|
99
|
+
elif len(sym) == 2:
|
|
100
|
+
sym = sym[0].upper() + sym[1].lower()
|
|
101
|
+
else:
|
|
102
|
+
return None
|
|
103
|
+
if sym not in SYMBOL_TO_Z:
|
|
104
|
+
return None
|
|
105
|
+
return sym
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ===================================================================
|
|
109
|
+
# Flow 1: Build from SMILES
|
|
110
|
+
# ===================================================================
|
|
111
|
+
|
|
112
|
+
def _flow_smiles():
|
|
113
|
+
"""Prompt for a SMILES string, parse it, and return the Molecule."""
|
|
114
|
+
print()
|
|
115
|
+
print(" Enter a SMILES string (e.g. CCO, c1ccccc1, CC(=O)O).")
|
|
116
|
+
smiles_str = _prompt(" SMILES: ")
|
|
117
|
+
if not smiles_str:
|
|
118
|
+
print(" No input provided.")
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
from molbuilder.smiles import parse
|
|
123
|
+
mol = parse(smiles_str)
|
|
124
|
+
mol.name = mol.name if mol.name else smiles_str
|
|
125
|
+
print()
|
|
126
|
+
print(f" Successfully parsed: {smiles_str}")
|
|
127
|
+
print(f" Atoms: {len(mol.atoms)} Bonds: {len(mol.bonds)}")
|
|
128
|
+
return mol
|
|
129
|
+
except Exception as exc:
|
|
130
|
+
print(f" Error parsing SMILES: {exc}")
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ===================================================================
|
|
135
|
+
# Flow 2: Build from molecular formula (VSEPR)
|
|
136
|
+
# ===================================================================
|
|
137
|
+
|
|
138
|
+
def _flow_formula():
|
|
139
|
+
"""Prompt for a molecular formula and build via VSEPR."""
|
|
140
|
+
print()
|
|
141
|
+
print(" Enter a molecular formula for a simple molecule")
|
|
142
|
+
print(" (e.g. H2O, CH4, NH3, BF3, SF6).")
|
|
143
|
+
formula = _prompt(" Formula: ")
|
|
144
|
+
if not formula:
|
|
145
|
+
print(" No input provided.")
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
from molbuilder.bonding.vsepr import VSEPRMolecule
|
|
150
|
+
vsepr = VSEPRMolecule(formula)
|
|
151
|
+
print()
|
|
152
|
+
print(vsepr.summary())
|
|
153
|
+
|
|
154
|
+
# Convert to a Molecule graph object for the analysis menu
|
|
155
|
+
coords = vsepr.coordinates
|
|
156
|
+
from molbuilder.molecule.graph import Molecule
|
|
157
|
+
mol = Molecule(formula)
|
|
158
|
+
for sym, pos in coords["atom_positions"]:
|
|
159
|
+
mol.add_atom(sym, pos)
|
|
160
|
+
for i, j, order in coords["bonds"]:
|
|
161
|
+
mol.add_bond(i, j, order=order)
|
|
162
|
+
mol.name = formula
|
|
163
|
+
return mol
|
|
164
|
+
except Exception as exc:
|
|
165
|
+
print(f" Error building molecule: {exc}")
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# ===================================================================
|
|
170
|
+
# Flow 3: Step-by-step atom-by-atom builder
|
|
171
|
+
# ===================================================================
|
|
172
|
+
|
|
173
|
+
def _flow_step_by_step():
|
|
174
|
+
"""Interactively build a molecule atom by atom."""
|
|
175
|
+
from molbuilder.molecule.graph import Molecule
|
|
176
|
+
|
|
177
|
+
print()
|
|
178
|
+
print(" Step-by-step Molecule Builder")
|
|
179
|
+
print(" " + "-" * 40)
|
|
180
|
+
print()
|
|
181
|
+
|
|
182
|
+
# First atom
|
|
183
|
+
sym = _prompt(" Enter element symbol for the first atom (e.g. C): ")
|
|
184
|
+
sym = _validate_element(sym) if sym else None
|
|
185
|
+
if sym is None:
|
|
186
|
+
print(" Invalid element symbol.")
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
mol = Molecule("custom molecule")
|
|
190
|
+
mol.add_atom(sym, [0.0, 0.0, 0.0])
|
|
191
|
+
print(f" Added {sym} as atom 0.")
|
|
192
|
+
|
|
193
|
+
while True:
|
|
194
|
+
print()
|
|
195
|
+
n_atoms = len(mol.atoms)
|
|
196
|
+
n_bonds = len(mol.bonds)
|
|
197
|
+
print(f" Current molecule: {n_atoms} atom(s), {n_bonds} bond(s)")
|
|
198
|
+
|
|
199
|
+
# Show atom list compactly
|
|
200
|
+
atom_list = ", ".join(
|
|
201
|
+
f"{a.symbol}[{a.index}]" for a in mol.atoms
|
|
202
|
+
)
|
|
203
|
+
if len(atom_list) > 60:
|
|
204
|
+
atom_list = atom_list[:57] + "..."
|
|
205
|
+
print(f" Atoms: {atom_list}")
|
|
206
|
+
print()
|
|
207
|
+
print(" Options:")
|
|
208
|
+
print(" [1] Add atom bonded to existing atom")
|
|
209
|
+
print(" [2] Add free (unconnected) atom")
|
|
210
|
+
print(" [3] Remove last atom")
|
|
211
|
+
print(" [4] Show current structure")
|
|
212
|
+
print(" [5] Done -- go to analysis")
|
|
213
|
+
print(" [0] Cancel -- back to wizard menu")
|
|
214
|
+
print()
|
|
215
|
+
|
|
216
|
+
choice = _prompt(" Choice: ")
|
|
217
|
+
|
|
218
|
+
if choice == "1":
|
|
219
|
+
_step_add_bonded(mol)
|
|
220
|
+
elif choice == "2":
|
|
221
|
+
_step_add_free(mol)
|
|
222
|
+
elif choice == "3":
|
|
223
|
+
_step_remove_last(mol)
|
|
224
|
+
elif choice == "4":
|
|
225
|
+
print()
|
|
226
|
+
print(mol.summary())
|
|
227
|
+
elif choice == "5":
|
|
228
|
+
if len(mol.atoms) == 0:
|
|
229
|
+
print(" Molecule is empty. Add at least one atom first.")
|
|
230
|
+
continue
|
|
231
|
+
return mol
|
|
232
|
+
elif choice == "0":
|
|
233
|
+
return None
|
|
234
|
+
else:
|
|
235
|
+
print(" Invalid choice. Please enter 0-5.")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _step_add_bonded(mol):
|
|
239
|
+
"""Add an atom bonded to an existing atom."""
|
|
240
|
+
if len(mol.atoms) == 0:
|
|
241
|
+
print(" No atoms yet. Add a free atom first.")
|
|
242
|
+
return
|
|
243
|
+
|
|
244
|
+
# Pick parent atom
|
|
245
|
+
n = len(mol.atoms)
|
|
246
|
+
if n == 1:
|
|
247
|
+
parent_idx = 0
|
|
248
|
+
print(f" Bonding to the only atom: {mol.atoms[0].symbol}[0]")
|
|
249
|
+
else:
|
|
250
|
+
print(f" Available atoms: 0 - {n - 1}")
|
|
251
|
+
parent_val = _prompt_int(f" Bond to which atom index? [0-{n-1}]: ", 0, n - 1)
|
|
252
|
+
if parent_val is None:
|
|
253
|
+
return
|
|
254
|
+
parent_idx = parent_val
|
|
255
|
+
|
|
256
|
+
# Element
|
|
257
|
+
sym = _prompt(" Element symbol for new atom (e.g. H, C, O): ")
|
|
258
|
+
sym = _validate_element(sym) if sym else None
|
|
259
|
+
if sym is None:
|
|
260
|
+
print(" Invalid element symbol.")
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
# Bond order
|
|
264
|
+
order_raw = _prompt(" Bond order (1=single, 2=double, 3=triple) [1]: ", "1")
|
|
265
|
+
try:
|
|
266
|
+
order = int(order_raw)
|
|
267
|
+
if order not in (1, 2, 3):
|
|
268
|
+
print(" Invalid bond order. Using single bond.")
|
|
269
|
+
order = 1
|
|
270
|
+
except ValueError:
|
|
271
|
+
order = 1
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
idx = mol.add_atom_bonded(sym, parent_idx, bond_order=order)
|
|
275
|
+
print(f" Added {sym}[{idx}] bonded to "
|
|
276
|
+
f"{mol.atoms[parent_idx].symbol}[{parent_idx}] "
|
|
277
|
+
f"(order={order}).")
|
|
278
|
+
except Exception as exc:
|
|
279
|
+
print(f" Error: {exc}")
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _step_add_free(mol):
|
|
283
|
+
"""Add a free (unconnected) atom."""
|
|
284
|
+
sym = _prompt(" Element symbol (e.g. C, N, O): ")
|
|
285
|
+
sym = _validate_element(sym) if sym else None
|
|
286
|
+
if sym is None:
|
|
287
|
+
print(" Invalid element symbol.")
|
|
288
|
+
return
|
|
289
|
+
|
|
290
|
+
# Place slightly offset from existing atoms
|
|
291
|
+
offset = len(mol.atoms) * 2.0
|
|
292
|
+
idx = mol.add_atom(sym, [offset, 0.0, 0.0])
|
|
293
|
+
print(f" Added free atom {sym}[{idx}].")
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _step_remove_last(mol):
|
|
297
|
+
"""Remove the last atom (and any bonds to it)."""
|
|
298
|
+
if len(mol.atoms) == 0:
|
|
299
|
+
print(" No atoms to remove.")
|
|
300
|
+
return
|
|
301
|
+
|
|
302
|
+
last_idx = len(mol.atoms) - 1
|
|
303
|
+
last_sym = mol.atoms[last_idx].symbol
|
|
304
|
+
|
|
305
|
+
# Remove bonds involving the last atom
|
|
306
|
+
mol.bonds = [b for b in mol.bonds
|
|
307
|
+
if b.atom_i != last_idx and b.atom_j != last_idx]
|
|
308
|
+
# Update adjacency
|
|
309
|
+
for nbr in list(mol._adj.get(last_idx, [])):
|
|
310
|
+
mol._adj[nbr] = [x for x in mol._adj[nbr] if x != last_idx]
|
|
311
|
+
if last_idx in mol._adj:
|
|
312
|
+
del mol._adj[last_idx]
|
|
313
|
+
# Remove the atom
|
|
314
|
+
mol.atoms.pop()
|
|
315
|
+
print(f" Removed {last_sym}[{last_idx}].")
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
# ===================================================================
|
|
319
|
+
# Flow 4: Preset molecules
|
|
320
|
+
# ===================================================================
|
|
321
|
+
|
|
322
|
+
_PRESETS = [
|
|
323
|
+
("Ethane (staggered)", "builder", "ethane"),
|
|
324
|
+
("Butane (anti)", "builder", "butane"),
|
|
325
|
+
("Cyclohexane (chair)", "builder", "cyclohexane"),
|
|
326
|
+
("2-Butene (cis/Z)", "builder", "2-butene"),
|
|
327
|
+
("Chiral molecule (CHFClBr)", "builder", "chiral"),
|
|
328
|
+
("Ethanol", "smiles", "CCO"),
|
|
329
|
+
("Acetic acid", "smiles", "CC(=O)O"),
|
|
330
|
+
("Benzene", "smiles", "c1ccccc1"),
|
|
331
|
+
("Aspirin", "smiles", "CC(=O)Oc1ccccc1C(=O)O"),
|
|
332
|
+
("Caffeine", "smiles", "Cn1cnc2c1c(=O)n(c(=O)n2C)C"),
|
|
333
|
+
]
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _flow_presets():
|
|
337
|
+
"""Let the user pick a preset molecule."""
|
|
338
|
+
print()
|
|
339
|
+
print(" Available preset molecules:")
|
|
340
|
+
print()
|
|
341
|
+
for i, (name, _, _) in enumerate(_PRESETS, 1):
|
|
342
|
+
print(f" [{i:>2}] {name}")
|
|
343
|
+
print(f" [ 0] Cancel")
|
|
344
|
+
print()
|
|
345
|
+
|
|
346
|
+
choice = _prompt_int(" Select preset: ", 0, len(_PRESETS))
|
|
347
|
+
if choice is None or choice == 0:
|
|
348
|
+
return None
|
|
349
|
+
|
|
350
|
+
name, kind, key = _PRESETS[choice - 1]
|
|
351
|
+
|
|
352
|
+
try:
|
|
353
|
+
if kind == "builder":
|
|
354
|
+
mol = _build_preset(key)
|
|
355
|
+
else:
|
|
356
|
+
from molbuilder.smiles import parse
|
|
357
|
+
mol = parse(key)
|
|
358
|
+
mol.name = name
|
|
359
|
+
print(f" Built: {mol.name} ({len(mol.atoms)} atoms, {len(mol.bonds)} bonds)")
|
|
360
|
+
return mol
|
|
361
|
+
except Exception as exc:
|
|
362
|
+
print(f" Error building {name}: {exc}")
|
|
363
|
+
return None
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _build_preset(key: str):
|
|
367
|
+
"""Build a preset molecule by key."""
|
|
368
|
+
from molbuilder.molecule.builders import (
|
|
369
|
+
build_ethane, build_butane, build_cyclohexane,
|
|
370
|
+
build_2_butene, build_chiral_molecule,
|
|
371
|
+
)
|
|
372
|
+
builders = {
|
|
373
|
+
"ethane": build_ethane,
|
|
374
|
+
"butane": build_butane,
|
|
375
|
+
"cyclohexane": build_cyclohexane,
|
|
376
|
+
"2-butene": build_2_butene,
|
|
377
|
+
"chiral": build_chiral_molecule,
|
|
378
|
+
}
|
|
379
|
+
return builders[key]()
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
# ===================================================================
|
|
383
|
+
# Flow 5: Peptide builder
|
|
384
|
+
# ===================================================================
|
|
385
|
+
|
|
386
|
+
def _parse_amino_acid_token(token: str):
|
|
387
|
+
"""Parse a single amino acid token. Returns an AminoAcidType or None."""
|
|
388
|
+
from molbuilder.molecule.amino_acids import AminoAcidType
|
|
389
|
+
|
|
390
|
+
upper = token.upper()
|
|
391
|
+
|
|
392
|
+
# Three-letter code
|
|
393
|
+
try:
|
|
394
|
+
return AminoAcidType[upper]
|
|
395
|
+
except (KeyError, ValueError):
|
|
396
|
+
pass
|
|
397
|
+
|
|
398
|
+
# One-letter code
|
|
399
|
+
if len(upper) == 1 and upper in _ONE_TO_THREE:
|
|
400
|
+
return AminoAcidType[_ONE_TO_THREE[upper]]
|
|
401
|
+
|
|
402
|
+
# Full name
|
|
403
|
+
if upper in _NAME_TO_THREE:
|
|
404
|
+
return AminoAcidType[_NAME_TO_THREE[upper]]
|
|
405
|
+
|
|
406
|
+
return None
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _flow_peptide():
|
|
410
|
+
"""Build a peptide from an amino acid sequence."""
|
|
411
|
+
print()
|
|
412
|
+
print(" Peptide Builder")
|
|
413
|
+
print(" " + "-" * 40)
|
|
414
|
+
print()
|
|
415
|
+
print(" Enter amino acid sequence using any of these formats:")
|
|
416
|
+
print(" - Three-letter codes: ALA GLY PHE")
|
|
417
|
+
print(" - One-letter codes: A G F")
|
|
418
|
+
print(" - Full names: Alanine Glycine Phenylalanine")
|
|
419
|
+
print(" - Or a mix: ALA G Phenylalanine")
|
|
420
|
+
print()
|
|
421
|
+
|
|
422
|
+
raw = _prompt(" Sequence: ")
|
|
423
|
+
if not raw:
|
|
424
|
+
print(" No input provided.")
|
|
425
|
+
return None
|
|
426
|
+
|
|
427
|
+
tokens = raw.split()
|
|
428
|
+
sequence = []
|
|
429
|
+
for tok in tokens:
|
|
430
|
+
aa = _parse_amino_acid_token(tok)
|
|
431
|
+
if aa is None:
|
|
432
|
+
print(f" Unrecognised amino acid: '{tok}'")
|
|
433
|
+
print(" Valid three-letter codes: " + ", ".join(
|
|
434
|
+
t.name for t in __import__(
|
|
435
|
+
"molbuilder.molecule.amino_acids",
|
|
436
|
+
fromlist=["AminoAcidType"]).AminoAcidType))
|
|
437
|
+
return None
|
|
438
|
+
sequence.append(aa)
|
|
439
|
+
|
|
440
|
+
if not sequence:
|
|
441
|
+
print(" Empty sequence.")
|
|
442
|
+
return None
|
|
443
|
+
|
|
444
|
+
names = [aa.name for aa in sequence]
|
|
445
|
+
print(f" Parsed sequence: {' - '.join(names)} ({len(sequence)} residues)")
|
|
446
|
+
|
|
447
|
+
# Optional secondary structure
|
|
448
|
+
print()
|
|
449
|
+
print(" Set secondary structure (optional):")
|
|
450
|
+
print(" [1] Alpha helix")
|
|
451
|
+
print(" [2] Beta sheet")
|
|
452
|
+
print(" [3] Extended")
|
|
453
|
+
print(" [0] None (default backbone angles)")
|
|
454
|
+
print()
|
|
455
|
+
ss_choice = _prompt(" Secondary structure [0]: ", "0")
|
|
456
|
+
|
|
457
|
+
phi_psi = None
|
|
458
|
+
ss_label = ""
|
|
459
|
+
if ss_choice == "1":
|
|
460
|
+
phi_psi = [(-57.0, -47.0)] * len(sequence)
|
|
461
|
+
ss_label = " (alpha helix)"
|
|
462
|
+
elif ss_choice == "2":
|
|
463
|
+
phi_psi = [(-135.0, 135.0)] * len(sequence)
|
|
464
|
+
ss_label = " (beta sheet)"
|
|
465
|
+
elif ss_choice == "3":
|
|
466
|
+
phi_psi = [(-180.0, 180.0)] * len(sequence)
|
|
467
|
+
ss_label = " (extended)"
|
|
468
|
+
|
|
469
|
+
try:
|
|
470
|
+
from molbuilder.molecule.amino_acids import build_peptide
|
|
471
|
+
mol = build_peptide(sequence, phi_psi=phi_psi)
|
|
472
|
+
mol.name = "-".join(names) + ss_label
|
|
473
|
+
print(f" Built peptide: {mol.name}")
|
|
474
|
+
print(f" Atoms: {len(mol.atoms)} Bonds: {len(mol.bonds)}")
|
|
475
|
+
return mol
|
|
476
|
+
except Exception as exc:
|
|
477
|
+
print(f" Error building peptide: {exc}")
|
|
478
|
+
return None
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
# ===================================================================
|
|
482
|
+
# Analysis menu
|
|
483
|
+
# ===================================================================
|
|
484
|
+
|
|
485
|
+
def _analysis_menu(mol):
|
|
486
|
+
"""Post-build analysis options for a molecule."""
|
|
487
|
+
while True:
|
|
488
|
+
print()
|
|
489
|
+
print(" " + "=" * 56)
|
|
490
|
+
print(" Analysis Options")
|
|
491
|
+
print(" " + "=" * 56)
|
|
492
|
+
formula = _molecular_formula(mol)
|
|
493
|
+
print(f" Current: {mol.name} ({len(mol.atoms)} atoms, "
|
|
494
|
+
f"{len(mol.bonds)} bonds)")
|
|
495
|
+
print()
|
|
496
|
+
print(" [ 1] Show molecule summary")
|
|
497
|
+
print(" [ 2] Show molecular formula and weight")
|
|
498
|
+
print(" [ 3] Detect functional groups")
|
|
499
|
+
print(" [ 4] Bond analysis")
|
|
500
|
+
print(" [ 5] Generate SMILES")
|
|
501
|
+
print(" [ 6] Export to file (XYZ/MOL/PDB/JSON)")
|
|
502
|
+
print(" [ 7] Retrosynthetic analysis")
|
|
503
|
+
print(" [ 8] Process engineering analysis")
|
|
504
|
+
print(" [ 9] Visualize 3D (requires display)")
|
|
505
|
+
print(" [10] Build another molecule")
|
|
506
|
+
print(" [ 0] Back to main menu")
|
|
507
|
+
print()
|
|
508
|
+
|
|
509
|
+
choice = _prompt(" Select option: ")
|
|
510
|
+
|
|
511
|
+
if choice == "0":
|
|
512
|
+
return False # back to main menu
|
|
513
|
+
elif choice == "1":
|
|
514
|
+
_analysis_summary(mol)
|
|
515
|
+
elif choice == "2":
|
|
516
|
+
_analysis_formula_weight(mol)
|
|
517
|
+
elif choice == "3":
|
|
518
|
+
_analysis_functional_groups(mol)
|
|
519
|
+
elif choice == "4":
|
|
520
|
+
_analysis_bonds(mol)
|
|
521
|
+
elif choice == "5":
|
|
522
|
+
_analysis_smiles(mol)
|
|
523
|
+
elif choice == "6":
|
|
524
|
+
_analysis_export(mol)
|
|
525
|
+
elif choice == "7":
|
|
526
|
+
_analysis_retrosynthesis(mol)
|
|
527
|
+
elif choice == "8":
|
|
528
|
+
_analysis_process(mol)
|
|
529
|
+
elif choice == "9":
|
|
530
|
+
_analysis_visualize(mol)
|
|
531
|
+
elif choice == "10":
|
|
532
|
+
return True # build another
|
|
533
|
+
else:
|
|
534
|
+
print(" Invalid choice. Please enter 0-10.")
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def _analysis_summary(mol):
|
|
538
|
+
"""Print the full molecule summary."""
|
|
539
|
+
print()
|
|
540
|
+
print(mol.summary())
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _analysis_formula_weight(mol):
|
|
544
|
+
"""Print molecular formula and weight."""
|
|
545
|
+
print()
|
|
546
|
+
formula = _molecular_formula(mol)
|
|
547
|
+
weight = _molecular_weight(mol)
|
|
548
|
+
print(f" Molecular formula: {formula}")
|
|
549
|
+
print(f" Molecular weight: {weight:.3f} g/mol")
|
|
550
|
+
print(f" Atom count: {len(mol.atoms)}")
|
|
551
|
+
print(f" Bond count: {len(mol.bonds)}")
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _analysis_functional_groups(mol):
|
|
555
|
+
"""Detect and display functional groups."""
|
|
556
|
+
print()
|
|
557
|
+
try:
|
|
558
|
+
from molbuilder.reactions.functional_group_detect import detect_functional_groups
|
|
559
|
+
groups = detect_functional_groups(mol)
|
|
560
|
+
if groups:
|
|
561
|
+
print(f" Functional groups found ({len(groups)}):")
|
|
562
|
+
for fg in groups:
|
|
563
|
+
atoms_str = ", ".join(str(a) for a in fg.atoms)
|
|
564
|
+
print(f" - {fg.name:<20s} atoms: [{atoms_str}]")
|
|
565
|
+
else:
|
|
566
|
+
print(" No standard functional groups detected.")
|
|
567
|
+
except Exception as exc:
|
|
568
|
+
print(f" Error detecting functional groups: {exc}")
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def _analysis_bonds(mol):
|
|
572
|
+
"""Display bond analysis."""
|
|
573
|
+
print()
|
|
574
|
+
if not mol.bonds:
|
|
575
|
+
print(" No bonds in molecule.")
|
|
576
|
+
return
|
|
577
|
+
|
|
578
|
+
print(f" Bond Analysis ({len(mol.bonds)} bonds):")
|
|
579
|
+
print(f" {'Atoms':<16} {'Order':>6} {'Length (A)':>11} {'Rotatable':>10}")
|
|
580
|
+
print(f" {'-' * 46}")
|
|
581
|
+
for bond in mol.bonds:
|
|
582
|
+
sa = mol.atoms[bond.atom_i].symbol
|
|
583
|
+
sb = mol.atoms[bond.atom_j].symbol
|
|
584
|
+
sym = {1: "single", 2: "double", 3: "triple"}.get(bond.order, "?")
|
|
585
|
+
dist = mol.distance(bond.atom_i, bond.atom_j)
|
|
586
|
+
rot = "yes" if bond.rotatable else "no"
|
|
587
|
+
label = f"{sa}[{bond.atom_i}]-{sb}[{bond.atom_j}]"
|
|
588
|
+
print(f" {label:<16} {sym:>6} {dist:>11.3f} {rot:>10}")
|
|
589
|
+
|
|
590
|
+
# Summary counts
|
|
591
|
+
from collections import Counter
|
|
592
|
+
order_counts = Counter(b.order for b in mol.bonds)
|
|
593
|
+
print()
|
|
594
|
+
print(" Bond order summary:")
|
|
595
|
+
for order in sorted(order_counts):
|
|
596
|
+
label = {1: "Single", 2: "Double", 3: "Triple"}.get(order, f"Order {order}")
|
|
597
|
+
print(f" {label}: {order_counts[order]}")
|
|
598
|
+
rot_count = sum(1 for b in mol.bonds if b.rotatable)
|
|
599
|
+
print(f" Rotatable bonds: {rot_count}")
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def _analysis_smiles(mol):
|
|
603
|
+
"""Generate and display a SMILES string."""
|
|
604
|
+
print()
|
|
605
|
+
try:
|
|
606
|
+
from molbuilder.smiles import to_smiles
|
|
607
|
+
smi = to_smiles(mol)
|
|
608
|
+
print(f" SMILES: {smi}")
|
|
609
|
+
except Exception as exc:
|
|
610
|
+
print(f" Error generating SMILES: {exc}")
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _analysis_export(mol):
|
|
614
|
+
"""Export molecule to a file in the chosen format."""
|
|
615
|
+
print()
|
|
616
|
+
print(" Export formats:")
|
|
617
|
+
print(" [1] XYZ")
|
|
618
|
+
print(" [2] MOL (V2000)")
|
|
619
|
+
print(" [3] PDB")
|
|
620
|
+
print(" [4] JSON")
|
|
621
|
+
print(" [0] Cancel")
|
|
622
|
+
print()
|
|
623
|
+
|
|
624
|
+
fmt_choice = _prompt(" Format: ")
|
|
625
|
+
if fmt_choice == "0" or not fmt_choice:
|
|
626
|
+
return
|
|
627
|
+
|
|
628
|
+
fmt_map = {
|
|
629
|
+
"1": ("xyz", "xyz"),
|
|
630
|
+
"2": ("mol", "mol"),
|
|
631
|
+
"3": ("pdb", "pdb"),
|
|
632
|
+
"4": ("json", "json"),
|
|
633
|
+
}
|
|
634
|
+
if fmt_choice not in fmt_map:
|
|
635
|
+
print(" Invalid format choice.")
|
|
636
|
+
return
|
|
637
|
+
|
|
638
|
+
fmt_name, ext = fmt_map[fmt_choice]
|
|
639
|
+
|
|
640
|
+
# Suggest a default filename
|
|
641
|
+
safe_name = mol.name.replace(" ", "_").replace("/", "-")
|
|
642
|
+
safe_name = "".join(c for c in safe_name if c.isalnum() or c in "_-.()")
|
|
643
|
+
if not safe_name:
|
|
644
|
+
safe_name = "molecule"
|
|
645
|
+
default_fn = f"{safe_name}.{ext}"
|
|
646
|
+
|
|
647
|
+
filepath = _prompt(f" Filename [{default_fn}]: ", default_fn)
|
|
648
|
+
|
|
649
|
+
try:
|
|
650
|
+
from molbuilder.io import write_xyz, write_mol, write_pdb, write_json
|
|
651
|
+
writers = {
|
|
652
|
+
"xyz": write_xyz,
|
|
653
|
+
"mol": write_mol,
|
|
654
|
+
"pdb": write_pdb,
|
|
655
|
+
"json": write_json,
|
|
656
|
+
}
|
|
657
|
+
writers[fmt_name](mol, filepath)
|
|
658
|
+
print(f" Exported to: {filepath}")
|
|
659
|
+
except Exception as exc:
|
|
660
|
+
print(f" Error exporting: {exc}")
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def _analysis_retrosynthesis(mol):
|
|
664
|
+
"""Run retrosynthetic analysis (if module available)."""
|
|
665
|
+
print()
|
|
666
|
+
try:
|
|
667
|
+
from molbuilder.reactions import (
|
|
668
|
+
detect_functional_groups, lookup_by_functional_group,
|
|
669
|
+
)
|
|
670
|
+
groups = detect_functional_groups(mol)
|
|
671
|
+
if not groups:
|
|
672
|
+
print(" No functional groups detected for retrosynthetic analysis.")
|
|
673
|
+
return
|
|
674
|
+
|
|
675
|
+
print(" Retrosynthetic Analysis")
|
|
676
|
+
print(" " + "-" * 40)
|
|
677
|
+
print(f" Detected functional groups: {len(groups)}")
|
|
678
|
+
for fg in groups:
|
|
679
|
+
print(f" - {fg.name}")
|
|
680
|
+
|
|
681
|
+
print()
|
|
682
|
+
print(" Suggested disconnections / reactions:")
|
|
683
|
+
found_any = False
|
|
684
|
+
for fg in groups:
|
|
685
|
+
templates = lookup_by_functional_group(fg.name)
|
|
686
|
+
for tmpl in templates:
|
|
687
|
+
print(f" [{fg.name}] {tmpl.name}")
|
|
688
|
+
if tmpl.description:
|
|
689
|
+
print(f" {tmpl.description}")
|
|
690
|
+
found_any = True
|
|
691
|
+
|
|
692
|
+
if not found_any:
|
|
693
|
+
print(" No reaction templates found for detected groups.")
|
|
694
|
+
|
|
695
|
+
except ImportError:
|
|
696
|
+
print(" Retrosynthetic analysis module is not yet available.")
|
|
697
|
+
print(" This feature requires molbuilder.reactions to be fully")
|
|
698
|
+
print(" implemented with retrosynthesis planning capabilities.")
|
|
699
|
+
except Exception as exc:
|
|
700
|
+
print(f" Error in retrosynthetic analysis: {exc}")
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
def _analysis_process(mol):
|
|
704
|
+
"""Run process engineering analysis (if module available)."""
|
|
705
|
+
print()
|
|
706
|
+
try:
|
|
707
|
+
from molbuilder.process.reactor import ReactorType
|
|
708
|
+
from molbuilder.reactions import (
|
|
709
|
+
detect_functional_groups, lookup_by_functional_group,
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
groups = detect_functional_groups(mol)
|
|
713
|
+
print(" Process Engineering Analysis")
|
|
714
|
+
print(" " + "-" * 40)
|
|
715
|
+
formula = _molecular_formula(mol)
|
|
716
|
+
weight = _molecular_weight(mol)
|
|
717
|
+
print(f" Molecule: {mol.name}")
|
|
718
|
+
print(f" Formula: {formula}")
|
|
719
|
+
print(f" Mol weight: {weight:.3f} g/mol")
|
|
720
|
+
print(f" Atoms: {len(mol.atoms)}")
|
|
721
|
+
print(f" Bonds: {len(mol.bonds)}")
|
|
722
|
+
print()
|
|
723
|
+
|
|
724
|
+
if groups:
|
|
725
|
+
print(" Reactive functional groups:")
|
|
726
|
+
for fg in groups:
|
|
727
|
+
print(f" - {fg.name}")
|
|
728
|
+
print()
|
|
729
|
+
print(" Suggested reactor types for synthesis:")
|
|
730
|
+
for rt in ReactorType:
|
|
731
|
+
print(f" - {rt.name}")
|
|
732
|
+
else:
|
|
733
|
+
print(" No reactive functional groups detected.")
|
|
734
|
+
print(" Process analysis requires identifiable reaction sites.")
|
|
735
|
+
|
|
736
|
+
except ImportError:
|
|
737
|
+
print(" Process engineering module is not yet fully available.")
|
|
738
|
+
print(" This feature requires molbuilder.process to be fully")
|
|
739
|
+
print(" implemented with reactor selection and costing.")
|
|
740
|
+
except Exception as exc:
|
|
741
|
+
print(f" Error in process analysis: {exc}")
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
def _analysis_visualize(mol):
|
|
745
|
+
"""Launch 3D visualisation (requires matplotlib and display)."""
|
|
746
|
+
print()
|
|
747
|
+
try:
|
|
748
|
+
from molbuilder.bonding.vsepr import VSEPRMolecule
|
|
749
|
+
from molbuilder.visualization.molecule_viz import visualize_molecule
|
|
750
|
+
|
|
751
|
+
# The visualizer expects a VSEPRMolecule, but our mol has
|
|
752
|
+
# to_coordinates_dict(). Build a thin wrapper.
|
|
753
|
+
coords = mol.to_coordinates_dict()
|
|
754
|
+
|
|
755
|
+
class _MolWrapper:
|
|
756
|
+
"""Minimal wrapper to satisfy visualize_molecule signature."""
|
|
757
|
+
def __init__(self, name, coordinates):
|
|
758
|
+
self.formula = name
|
|
759
|
+
self.coordinates = coordinates
|
|
760
|
+
# Provide a minimal axe attribute
|
|
761
|
+
self.axe = type("AXE", (), {
|
|
762
|
+
"molecular_geometry": "custom",
|
|
763
|
+
"electron_geometry": "custom",
|
|
764
|
+
"hybridisation": "---",
|
|
765
|
+
"notation": "---",
|
|
766
|
+
"ideal_bond_angles": [],
|
|
767
|
+
})()
|
|
768
|
+
def computed_bond_angles(self):
|
|
769
|
+
return []
|
|
770
|
+
def summary(self):
|
|
771
|
+
return f" {self.formula}"
|
|
772
|
+
|
|
773
|
+
wrapper = _MolWrapper(mol.name, coords)
|
|
774
|
+
print(" Launching 3D visualisation...")
|
|
775
|
+
print(" (Close the figure window to return to the menu.)")
|
|
776
|
+
visualize_molecule(wrapper)
|
|
777
|
+
except ImportError as exc:
|
|
778
|
+
print(f" Visualisation requires matplotlib: {exc}")
|
|
779
|
+
print(" Install with: pip install matplotlib")
|
|
780
|
+
except Exception as exc:
|
|
781
|
+
print(f" Error during visualisation: {exc}")
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
# ===================================================================
|
|
785
|
+
# Main wizard entry point
|
|
786
|
+
# ===================================================================
|
|
787
|
+
|
|
788
|
+
def wizard_main():
|
|
789
|
+
"""Top-level interactive molecule building wizard."""
|
|
790
|
+
while True:
|
|
791
|
+
print()
|
|
792
|
+
print(" " + "=" * 56)
|
|
793
|
+
print(" Molecule Builder Wizard")
|
|
794
|
+
print(" " + "=" * 56)
|
|
795
|
+
print()
|
|
796
|
+
print(" [1] Build from SMILES string")
|
|
797
|
+
print(" [2] Build from molecular formula (simple molecules)")
|
|
798
|
+
print(" [3] Build step-by-step (atom by atom)")
|
|
799
|
+
print(" [4] Choose from preset molecules")
|
|
800
|
+
print(" [5] Build peptide from amino acid sequence")
|
|
801
|
+
print(" [6] Back to main menu")
|
|
802
|
+
print()
|
|
803
|
+
|
|
804
|
+
choice = _prompt(" Select option: ")
|
|
805
|
+
|
|
806
|
+
mol = None
|
|
807
|
+
|
|
808
|
+
if choice == "1":
|
|
809
|
+
mol = _flow_smiles()
|
|
810
|
+
elif choice == "2":
|
|
811
|
+
mol = _flow_formula()
|
|
812
|
+
elif choice == "3":
|
|
813
|
+
mol = _flow_step_by_step()
|
|
814
|
+
elif choice == "4":
|
|
815
|
+
mol = _flow_presets()
|
|
816
|
+
elif choice == "5":
|
|
817
|
+
mol = _flow_peptide()
|
|
818
|
+
elif choice == "6":
|
|
819
|
+
return
|
|
820
|
+
else:
|
|
821
|
+
print(" Invalid choice. Please enter 1-6.")
|
|
822
|
+
continue
|
|
823
|
+
|
|
824
|
+
if mol is None:
|
|
825
|
+
continue
|
|
826
|
+
|
|
827
|
+
# Enter analysis menu
|
|
828
|
+
build_another = _analysis_menu(mol)
|
|
829
|
+
if not build_another:
|
|
830
|
+
return # back to main menu
|
|
831
|
+
# else: loop back to wizard menu
|