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,433 @@
|
|
|
1
|
+
"""
|
|
2
|
+
VSEPR (Valence Shell Electron Pair Repulsion) Theory
|
|
3
|
+
|
|
4
|
+
Predicts molecular geometry from electron pair repulsion around a
|
|
5
|
+
central atom. Given a molecular formula, this module:
|
|
6
|
+
|
|
7
|
+
1. Builds a Lewis structure (bonding pairs + lone pairs)
|
|
8
|
+
2. Classifies as AXnEm (A=central, X=bonds, E=lone pairs)
|
|
9
|
+
3. Predicts electron geometry, molecular geometry, hybridisation
|
|
10
|
+
4. Generates 3D atomic coordinates for visualisation
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import math
|
|
14
|
+
import numpy as np
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
|
|
17
|
+
from molbuilder.core.element_properties import estimated_bond_length_pm
|
|
18
|
+
from molbuilder.bonding.lewis import LewisStructure
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ===================================================================
|
|
22
|
+
# Geometry reference tables
|
|
23
|
+
# ===================================================================
|
|
24
|
+
|
|
25
|
+
ELECTRON_GEOMETRY = {
|
|
26
|
+
1: "terminal",
|
|
27
|
+
2: "linear",
|
|
28
|
+
3: "trigonal planar",
|
|
29
|
+
4: "tetrahedral",
|
|
30
|
+
5: "trigonal bipyramidal",
|
|
31
|
+
6: "octahedral",
|
|
32
|
+
7: "pentagonal bipyramidal",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# (steric_number, lone_pairs) -> (molecular_geometry, ideal_bond_angles)
|
|
36
|
+
MOLECULAR_GEOMETRY = {
|
|
37
|
+
(1, 0): ("terminal", [0.0]),
|
|
38
|
+
(2, 0): ("linear", [180.0]),
|
|
39
|
+
(2, 1): ("linear", [180.0]),
|
|
40
|
+
(3, 0): ("trigonal planar", [120.0]),
|
|
41
|
+
(3, 1): ("bent", [117.0]),
|
|
42
|
+
(4, 0): ("tetrahedral", [109.5]),
|
|
43
|
+
(4, 1): ("trigonal pyramidal", [107.0]),
|
|
44
|
+
(4, 2): ("bent", [104.5]),
|
|
45
|
+
(5, 0): ("trigonal bipyramidal", [90.0, 120.0]),
|
|
46
|
+
(5, 1): ("seesaw", [90.0, 117.0]),
|
|
47
|
+
(5, 2): ("T-shaped", [90.0]),
|
|
48
|
+
(5, 3): ("linear", [180.0]),
|
|
49
|
+
(6, 0): ("octahedral", [90.0]),
|
|
50
|
+
(6, 1): ("square pyramidal", [90.0]),
|
|
51
|
+
(6, 2): ("square planar", [90.0]),
|
|
52
|
+
(6, 3): ("T-shaped", [90.0]),
|
|
53
|
+
(6, 4): ("linear", [180.0]),
|
|
54
|
+
(7, 0): ("pentagonal bipyramidal", [72.0, 90.0]),
|
|
55
|
+
(7, 1): ("pentagonal pyramidal", [72.0, 90.0]),
|
|
56
|
+
(7, 2): ("pentagonal planar", [72.0]),
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
HYBRIDIZATION = {
|
|
60
|
+
1: "s",
|
|
61
|
+
2: "sp",
|
|
62
|
+
3: "sp2",
|
|
63
|
+
4: "sp3",
|
|
64
|
+
5: "sp3d",
|
|
65
|
+
6: "sp3d2",
|
|
66
|
+
7: "sp3d3",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ===================================================================
|
|
71
|
+
# AXE classification
|
|
72
|
+
# ===================================================================
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class AXEClassification:
|
|
76
|
+
"""AXnEm classification of a molecule's central atom."""
|
|
77
|
+
central_symbol: str
|
|
78
|
+
bonding_groups: int # X
|
|
79
|
+
lone_pairs: int # E
|
|
80
|
+
steric_number: int # X + E
|
|
81
|
+
axe_notation: str # e.g. "AX2E2"
|
|
82
|
+
electron_geometry: str
|
|
83
|
+
molecular_geometry: str
|
|
84
|
+
ideal_bond_angles: list
|
|
85
|
+
hybridization: str
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ===================================================================
|
|
89
|
+
# Ideal direction vectors for each electron geometry
|
|
90
|
+
# ===================================================================
|
|
91
|
+
|
|
92
|
+
def _linear_directions():
|
|
93
|
+
return [
|
|
94
|
+
np.array([0.0, 0.0, 1.0]),
|
|
95
|
+
np.array([0.0, 0.0, -1.0]),
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _trigonal_planar_directions():
|
|
100
|
+
return [
|
|
101
|
+
np.array([1.0, 0.0, 0.0]),
|
|
102
|
+
np.array([-0.5, math.sqrt(3)/2, 0.0]),
|
|
103
|
+
np.array([-0.5, -math.sqrt(3)/2, 0.0]),
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _tetrahedral_directions():
|
|
108
|
+
return [
|
|
109
|
+
np.array([ 1.0, 1.0, 1.0]) / math.sqrt(3),
|
|
110
|
+
np.array([ 1.0, -1.0, -1.0]) / math.sqrt(3),
|
|
111
|
+
np.array([-1.0, 1.0, -1.0]) / math.sqrt(3),
|
|
112
|
+
np.array([-1.0, -1.0, 1.0]) / math.sqrt(3),
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _trigonal_bipyramidal_directions():
|
|
117
|
+
"""3 equatorial (xy-plane, 120 deg) + 2 axial (+z, -z)."""
|
|
118
|
+
eq = _trigonal_planar_directions()
|
|
119
|
+
ax = [np.array([0.0, 0.0, 1.0]), np.array([0.0, 0.0, -1.0])]
|
|
120
|
+
# Return equatorial first, then axial (lone pairs prefer equatorial)
|
|
121
|
+
return eq + ax
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _octahedral_directions():
|
|
125
|
+
return [
|
|
126
|
+
np.array([ 1.0, 0.0, 0.0]),
|
|
127
|
+
np.array([-1.0, 0.0, 0.0]),
|
|
128
|
+
np.array([ 0.0, 1.0, 0.0]),
|
|
129
|
+
np.array([ 0.0, -1.0, 0.0]),
|
|
130
|
+
np.array([ 0.0, 0.0, 1.0]),
|
|
131
|
+
np.array([ 0.0, 0.0, -1.0]),
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _pentagonal_bipyramidal_directions():
|
|
136
|
+
"""5 equatorial (xy-plane, 72 deg) + 2 axial."""
|
|
137
|
+
eq = []
|
|
138
|
+
for i in range(5):
|
|
139
|
+
angle = 2 * math.pi * i / 5
|
|
140
|
+
eq.append(np.array([math.cos(angle), math.sin(angle), 0.0]))
|
|
141
|
+
ax = [np.array([0.0, 0.0, 1.0]), np.array([0.0, 0.0, -1.0])]
|
|
142
|
+
return eq + ax
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
_DIRECTION_GENERATORS = {
|
|
146
|
+
2: _linear_directions,
|
|
147
|
+
3: _trigonal_planar_directions,
|
|
148
|
+
4: _tetrahedral_directions,
|
|
149
|
+
5: _trigonal_bipyramidal_directions,
|
|
150
|
+
6: _octahedral_directions,
|
|
151
|
+
7: _pentagonal_bipyramidal_directions,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _assign_positions(steric_number: int, num_lone_pairs: int):
|
|
156
|
+
"""Assign direction vectors to bonding groups and lone pairs.
|
|
157
|
+
|
|
158
|
+
Lone pairs are placed at positions that minimise 90-degree repulsions:
|
|
159
|
+
SN=3: lone pair at any equatorial position
|
|
160
|
+
SN=4: lone pair at any tetrahedral vertex
|
|
161
|
+
SN=5: lone pairs at equatorial positions first
|
|
162
|
+
SN=6: 1 LP at any position; 2 LPs at trans (opposite) positions
|
|
163
|
+
|
|
164
|
+
Returns
|
|
165
|
+
-------
|
|
166
|
+
bonding_dirs : list of np.array -- unit vectors for bonded atoms
|
|
167
|
+
lone_pair_dirs : list of np.array -- unit vectors for lone pairs
|
|
168
|
+
"""
|
|
169
|
+
gen = _DIRECTION_GENERATORS.get(steric_number)
|
|
170
|
+
if gen is None:
|
|
171
|
+
# Single atom or unknown: just put along +z
|
|
172
|
+
return [np.array([0.0, 0.0, 1.0])] * (steric_number - num_lone_pairs), \
|
|
173
|
+
[np.array([0.0, 0.0, -1.0])] * num_lone_pairs
|
|
174
|
+
|
|
175
|
+
all_dirs = gen()
|
|
176
|
+
|
|
177
|
+
if steric_number == 5:
|
|
178
|
+
# Equatorial positions are indices 0,1,2; axial are 3,4
|
|
179
|
+
# Lone pairs prefer equatorial
|
|
180
|
+
lp_indices = list(range(min(num_lone_pairs, 3)))
|
|
181
|
+
if num_lone_pairs > 3:
|
|
182
|
+
lp_indices.extend([3, 4][:num_lone_pairs - 3])
|
|
183
|
+
elif steric_number == 6:
|
|
184
|
+
# For 1 LP: remove index 0 (arbitrary, all equivalent)
|
|
185
|
+
# For 2 LPs: remove trans pair (indices 0,1 are +x/-x)
|
|
186
|
+
if num_lone_pairs == 1:
|
|
187
|
+
lp_indices = [0]
|
|
188
|
+
elif num_lone_pairs == 2:
|
|
189
|
+
lp_indices = [0, 1] # trans pair
|
|
190
|
+
elif num_lone_pairs == 3:
|
|
191
|
+
lp_indices = [0, 1, 2] # fac arrangement
|
|
192
|
+
elif num_lone_pairs == 4:
|
|
193
|
+
lp_indices = [0, 1, 2, 3]
|
|
194
|
+
else:
|
|
195
|
+
lp_indices = list(range(num_lone_pairs))
|
|
196
|
+
else:
|
|
197
|
+
# For SN=2,3,4: lone pairs take the last positions
|
|
198
|
+
lp_indices = list(range(steric_number - num_lone_pairs, steric_number))
|
|
199
|
+
|
|
200
|
+
bonding_dirs = [all_dirs[i] for i in range(len(all_dirs)) if i not in lp_indices]
|
|
201
|
+
lone_pair_dirs = [all_dirs[i] for i in lp_indices]
|
|
202
|
+
|
|
203
|
+
return bonding_dirs, lone_pair_dirs
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# ===================================================================
|
|
207
|
+
# 3D coordinate generation
|
|
208
|
+
# ===================================================================
|
|
209
|
+
|
|
210
|
+
def generate_3d_coordinates(lewis: LewisStructure) -> dict:
|
|
211
|
+
"""Generate 3D atomic coordinates from a Lewis structure.
|
|
212
|
+
|
|
213
|
+
Central atom is placed at the origin. Terminal atoms are placed
|
|
214
|
+
along ideal geometry direction vectors, scaled by estimated bond
|
|
215
|
+
lengths.
|
|
216
|
+
|
|
217
|
+
Returns
|
|
218
|
+
-------
|
|
219
|
+
dict with keys:
|
|
220
|
+
'atom_positions' : list of (symbol, np.array([x,y,z])) in Angstroms
|
|
221
|
+
'bonds' : list of (idx_a, idx_b, bond_order)
|
|
222
|
+
'lone_pair_positions': list of (atom_idx, np.array direction)
|
|
223
|
+
'central_index' : int
|
|
224
|
+
"""
|
|
225
|
+
sn = lewis.steric_number()
|
|
226
|
+
lp_count = lewis.lone_pairs_on_central()
|
|
227
|
+
bp_count = lewis.bonding_pairs_on_central()
|
|
228
|
+
|
|
229
|
+
# Handle single-atom or diatomic edge case
|
|
230
|
+
if sn < 2 and len(lewis.atoms) == 1:
|
|
231
|
+
return {
|
|
232
|
+
'atom_positions': [(lewis.atoms[0], np.array([0.0, 0.0, 0.0]))],
|
|
233
|
+
'bonds': [],
|
|
234
|
+
'lone_pair_positions': [],
|
|
235
|
+
'central_index': 0,
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
# Get direction vectors
|
|
239
|
+
if sn >= 2:
|
|
240
|
+
bonding_dirs, lp_dirs = _assign_positions(sn, lp_count)
|
|
241
|
+
else:
|
|
242
|
+
# Diatomic with SN=1
|
|
243
|
+
bonding_dirs = [np.array([0.0, 0.0, 1.0])]
|
|
244
|
+
lp_dirs = []
|
|
245
|
+
|
|
246
|
+
# Build atom positions
|
|
247
|
+
atom_positions = [(None, None)] * len(lewis.atoms)
|
|
248
|
+
atom_positions[lewis.central_index] = (
|
|
249
|
+
lewis.central_symbol,
|
|
250
|
+
np.array([0.0, 0.0, 0.0]),
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Assign each bonded terminal to a direction
|
|
254
|
+
bonds_out = []
|
|
255
|
+
bond_dir_idx = 0
|
|
256
|
+
for bond in lewis.bonds:
|
|
257
|
+
ti = bond.atom_b if bond.atom_a == lewis.central_index else bond.atom_a
|
|
258
|
+
sym = lewis.atoms[ti]
|
|
259
|
+
|
|
260
|
+
bl = estimated_bond_length_pm(lewis.central_symbol, sym, bond.order) / 100.0
|
|
261
|
+
|
|
262
|
+
if bond_dir_idx < len(bonding_dirs):
|
|
263
|
+
direction = bonding_dirs[bond_dir_idx]
|
|
264
|
+
else:
|
|
265
|
+
direction = np.array([0.0, 0.0, 1.0])
|
|
266
|
+
bond_dir_idx += 1
|
|
267
|
+
|
|
268
|
+
pos = direction * bl
|
|
269
|
+
atom_positions[ti] = (sym, pos)
|
|
270
|
+
bonds_out.append((lewis.central_index, ti, bond.order))
|
|
271
|
+
|
|
272
|
+
# Lone pair direction info (for visualisation)
|
|
273
|
+
lp_positions = []
|
|
274
|
+
for i, lp_dir in enumerate(lp_dirs):
|
|
275
|
+
lp_positions.append((lewis.central_index, lp_dir))
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
'atom_positions': atom_positions,
|
|
279
|
+
'bonds': bonds_out,
|
|
280
|
+
'lone_pair_positions': lp_positions,
|
|
281
|
+
'central_index': lewis.central_index,
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# ===================================================================
|
|
286
|
+
# VSEPRMolecule
|
|
287
|
+
# ===================================================================
|
|
288
|
+
|
|
289
|
+
class VSEPRMolecule:
|
|
290
|
+
"""Complete VSEPR analysis of a molecule.
|
|
291
|
+
|
|
292
|
+
Parameters
|
|
293
|
+
----------
|
|
294
|
+
formula : str
|
|
295
|
+
Molecular formula (e.g., 'H2O', 'CH4').
|
|
296
|
+
charge : int
|
|
297
|
+
Net charge (default 0).
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
def __init__(self, formula: str, charge: int = 0):
|
|
301
|
+
self.formula = formula
|
|
302
|
+
self.charge = charge
|
|
303
|
+
self.lewis = LewisStructure(formula, charge)
|
|
304
|
+
self.axe = self._classify()
|
|
305
|
+
self.coordinates = generate_3d_coordinates(self.lewis)
|
|
306
|
+
|
|
307
|
+
def _classify(self) -> AXEClassification:
|
|
308
|
+
"""Perform AXnEm classification."""
|
|
309
|
+
X = self.lewis.bonding_pairs_on_central()
|
|
310
|
+
E = self.lewis.lone_pairs_on_central()
|
|
311
|
+
sn = X + E
|
|
312
|
+
|
|
313
|
+
notation = f"AX{X}" + (f"E{E}" if E > 0 else "")
|
|
314
|
+
e_geom = ELECTRON_GEOMETRY.get(sn, "unknown")
|
|
315
|
+
|
|
316
|
+
entry = MOLECULAR_GEOMETRY.get((sn, E))
|
|
317
|
+
if entry:
|
|
318
|
+
m_geom, angles = entry
|
|
319
|
+
else:
|
|
320
|
+
m_geom, angles = "unknown", []
|
|
321
|
+
|
|
322
|
+
hybrid = HYBRIDIZATION.get(sn, "unknown")
|
|
323
|
+
|
|
324
|
+
return AXEClassification(
|
|
325
|
+
central_symbol=self.lewis.central_symbol,
|
|
326
|
+
bonding_groups=X,
|
|
327
|
+
lone_pairs=E,
|
|
328
|
+
steric_number=sn,
|
|
329
|
+
axe_notation=notation,
|
|
330
|
+
electron_geometry=e_geom,
|
|
331
|
+
molecular_geometry=m_geom,
|
|
332
|
+
ideal_bond_angles=angles,
|
|
333
|
+
hybridization=hybrid,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
def computed_bond_angles(self) -> list[float]:
|
|
337
|
+
"""Compute actual bond angles from 3D coordinates (degrees)."""
|
|
338
|
+
coords = self.coordinates
|
|
339
|
+
central_pos = coords['atom_positions'][coords['central_index']][1]
|
|
340
|
+
terminal_positions = []
|
|
341
|
+
for idx_a, idx_b, order in coords['bonds']:
|
|
342
|
+
ti = idx_b if idx_a == coords['central_index'] else idx_a
|
|
343
|
+
terminal_positions.append(coords['atom_positions'][ti][1])
|
|
344
|
+
|
|
345
|
+
angles = []
|
|
346
|
+
for i in range(len(terminal_positions)):
|
|
347
|
+
for j in range(i + 1, len(terminal_positions)):
|
|
348
|
+
va = terminal_positions[i] - central_pos
|
|
349
|
+
vb = terminal_positions[j] - central_pos
|
|
350
|
+
na, nb = np.linalg.norm(va), np.linalg.norm(vb)
|
|
351
|
+
if na < 1e-10 or nb < 1e-10:
|
|
352
|
+
continue
|
|
353
|
+
cos_angle = np.clip(np.dot(va, vb) / (na * nb), -1.0, 1.0)
|
|
354
|
+
angles.append(math.degrees(math.acos(cos_angle)))
|
|
355
|
+
return sorted(angles)
|
|
356
|
+
|
|
357
|
+
# ------ display ------
|
|
358
|
+
|
|
359
|
+
def __repr__(self):
|
|
360
|
+
return (f"VSEPRMolecule({self.formula}, "
|
|
361
|
+
f"{self.axe.axe_notation}, "
|
|
362
|
+
f"{self.axe.molecular_geometry})")
|
|
363
|
+
|
|
364
|
+
def summary(self) -> str:
|
|
365
|
+
"""Return a detailed ASCII summary."""
|
|
366
|
+
axe = self.axe
|
|
367
|
+
coords = self.coordinates
|
|
368
|
+
|
|
369
|
+
angles_str = ", ".join(f"{a:.1f}" for a in axe.ideal_bond_angles) if axe.ideal_bond_angles else "N/A"
|
|
370
|
+
computed = self.computed_bond_angles()
|
|
371
|
+
computed_str = ", ".join(f"{a:.1f}" for a in computed) if computed else "N/A"
|
|
372
|
+
|
|
373
|
+
lines = [
|
|
374
|
+
f"{'='*60}",
|
|
375
|
+
f" VSEPR Analysis: {self.formula}",
|
|
376
|
+
f" Charge: {self.charge:+d}",
|
|
377
|
+
f"{'='*60}",
|
|
378
|
+
f" Lewis Structure:",
|
|
379
|
+
f" Total valence electrons: {self.lewis.total_valence_electrons}",
|
|
380
|
+
f" Central atom: {self.lewis.central_symbol}",
|
|
381
|
+
]
|
|
382
|
+
|
|
383
|
+
# Bonds summary
|
|
384
|
+
bond_strs = []
|
|
385
|
+
for bond in self.lewis.bonds:
|
|
386
|
+
sym_a = self.lewis.atoms[bond.atom_a]
|
|
387
|
+
sym_b = self.lewis.atoms[bond.atom_b]
|
|
388
|
+
order_sym = {1: "-", 2: "=", 3: "#"}.get(bond.order, "?")
|
|
389
|
+
bond_strs.append(f"{sym_a}{order_sym}{sym_b}")
|
|
390
|
+
lines.append(f" Bonds: {', '.join(bond_strs)}")
|
|
391
|
+
|
|
392
|
+
lp_central = self.lewis.lone_pairs_on_central()
|
|
393
|
+
lines.append(f" Lone pairs on {self.lewis.central_symbol}: {lp_central}")
|
|
394
|
+
|
|
395
|
+
lines.extend([
|
|
396
|
+
f"{'='*60}",
|
|
397
|
+
f" VSEPR Classification:",
|
|
398
|
+
f" AXE notation: {axe.axe_notation}",
|
|
399
|
+
f" Steric number: {axe.steric_number}",
|
|
400
|
+
f" Electron geometry: {axe.electron_geometry}",
|
|
401
|
+
f" Molecular geometry: {axe.molecular_geometry}",
|
|
402
|
+
f" Hybridization: {axe.hybridization}",
|
|
403
|
+
f" Ideal bond angles: {angles_str} deg",
|
|
404
|
+
f" Computed angles: {computed_str} deg",
|
|
405
|
+
])
|
|
406
|
+
|
|
407
|
+
lines.append(f"{'='*60}")
|
|
408
|
+
lines.append(" 3D Coordinates (Angstroms):")
|
|
409
|
+
for i, (sym, pos) in enumerate(coords['atom_positions']):
|
|
410
|
+
if sym is not None and pos is not None:
|
|
411
|
+
tag = " <-- central" if i == coords['central_index'] else ""
|
|
412
|
+
lines.append(
|
|
413
|
+
f" {sym:<4} {pos[0]:>8.4f} {pos[1]:>8.4f} {pos[2]:>8.4f}{tag}"
|
|
414
|
+
)
|
|
415
|
+
lines.append(f"{'='*60}")
|
|
416
|
+
return "\n".join(lines)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
# ===================================================================
|
|
420
|
+
# Convenience constructors
|
|
421
|
+
# ===================================================================
|
|
422
|
+
|
|
423
|
+
def from_formula(formula: str, charge: int = 0) -> VSEPRMolecule:
|
|
424
|
+
"""Create a VSEPRMolecule from a molecular formula string."""
|
|
425
|
+
return VSEPRMolecule(formula, charge)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def from_atoms(symbols: list[str], charge: int = 0) -> VSEPRMolecule:
|
|
429
|
+
"""Create a VSEPRMolecule from a list of element symbols."""
|
|
430
|
+
from collections import Counter
|
|
431
|
+
counts = Counter(symbols)
|
|
432
|
+
formula = "".join(f"{sym}{cnt if cnt > 1 else ''}" for sym, cnt in counts.items())
|
|
433
|
+
return VSEPRMolecule(formula, charge)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Command-line interface: menus, demos, interactive wizard."""
|