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,442 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Covalent Bond Modelling
|
|
3
|
+
|
|
4
|
+
Models the different types of covalent bonds and their physical properties:
|
|
5
|
+
|
|
6
|
+
- Single, double, and triple bonds (sigma/pi decomposition)
|
|
7
|
+
- Polar vs nonpolar classification
|
|
8
|
+
- Coordinate (dative) bonds
|
|
9
|
+
- Bond length, dissociation energy, dipole moment estimation
|
|
10
|
+
- Molecular polarity analysis from bond vectors
|
|
11
|
+
|
|
12
|
+
Integrates with the existing Lewis structure and element data modules.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import math
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from enum import Enum, auto
|
|
20
|
+
|
|
21
|
+
from molbuilder.core.elements import SYMBOL_TO_Z
|
|
22
|
+
from molbuilder.core.element_properties import (
|
|
23
|
+
electronegativity,
|
|
24
|
+
estimated_bond_length_pm,
|
|
25
|
+
covalent_radius_pm,
|
|
26
|
+
PAULING_ELECTRONEGATIVITY,
|
|
27
|
+
)
|
|
28
|
+
from molbuilder.core.bond_data import BDE_TABLE, NONPOLAR_THRESHOLD, POLAR_COVALENT_MAX
|
|
29
|
+
from molbuilder.core.constants import DEBYE_PER_E_ANGSTROM
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ===================================================================
|
|
33
|
+
# Enumerations
|
|
34
|
+
# ===================================================================
|
|
35
|
+
|
|
36
|
+
class BondPolarity(Enum):
|
|
37
|
+
"""Classification of a bond by electronegativity difference."""
|
|
38
|
+
NONPOLAR_COVALENT = auto()
|
|
39
|
+
POLAR_COVALENT = auto()
|
|
40
|
+
IONIC = auto()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class OrbitalType(Enum):
|
|
44
|
+
"""Orbital overlap type contributing to a covalent bond."""
|
|
45
|
+
SIGMA = auto() # head-on overlap along the bond axis
|
|
46
|
+
PI = auto() # lateral overlap above/below the bond axis
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ===================================================================
|
|
50
|
+
# Sigma / Pi orbital composition
|
|
51
|
+
# ===================================================================
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class OrbitalContribution:
|
|
55
|
+
"""Describes one orbital overlap contributing to a bond."""
|
|
56
|
+
orbital_type: OrbitalType
|
|
57
|
+
description: str
|
|
58
|
+
|
|
59
|
+
def __repr__(self):
|
|
60
|
+
label = "sigma" if self.orbital_type is OrbitalType.SIGMA else "pi"
|
|
61
|
+
return f"{label}({self.description})"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def sigma_pi_composition(bond_order: int) -> list[OrbitalContribution]:
|
|
65
|
+
"""Return the sigma/pi orbital makeup for a given bond order.
|
|
66
|
+
|
|
67
|
+
Single bond: 1 sigma
|
|
68
|
+
Double bond: 1 sigma + 1 pi
|
|
69
|
+
Triple bond: 1 sigma + 2 pi
|
|
70
|
+
"""
|
|
71
|
+
if bond_order < 1 or bond_order > 3:
|
|
72
|
+
raise ValueError(f"Bond order must be 1, 2, or 3; got {bond_order}")
|
|
73
|
+
|
|
74
|
+
orbitals = [OrbitalContribution(OrbitalType.SIGMA,
|
|
75
|
+
"head-on overlap along bond axis")]
|
|
76
|
+
if bond_order >= 2:
|
|
77
|
+
orbitals.append(OrbitalContribution(OrbitalType.PI,
|
|
78
|
+
"lateral overlap, perpendicular plane 1"))
|
|
79
|
+
if bond_order == 3:
|
|
80
|
+
orbitals.append(OrbitalContribution(OrbitalType.PI,
|
|
81
|
+
"lateral overlap, perpendicular plane 2"))
|
|
82
|
+
return orbitals
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ===================================================================
|
|
86
|
+
# BDE key helper
|
|
87
|
+
# ===================================================================
|
|
88
|
+
|
|
89
|
+
def _bde_key(sym_a: str, sym_b: str, order: int) -> tuple[str, str, int]:
|
|
90
|
+
"""Normalise to alphabetical order for BDE table lookup."""
|
|
91
|
+
a, b = sorted([sym_a, sym_b])
|
|
92
|
+
return (a, b, order)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ===================================================================
|
|
96
|
+
# CovalentBond
|
|
97
|
+
# ===================================================================
|
|
98
|
+
|
|
99
|
+
@dataclass
|
|
100
|
+
class CovalentBond:
|
|
101
|
+
"""Model of a covalent bond between two atoms.
|
|
102
|
+
|
|
103
|
+
Parameters
|
|
104
|
+
----------
|
|
105
|
+
symbol_a : str
|
|
106
|
+
Element symbol of first atom.
|
|
107
|
+
symbol_b : str
|
|
108
|
+
Element symbol of second atom.
|
|
109
|
+
bond_order : int
|
|
110
|
+
1 = single, 2 = double, 3 = triple.
|
|
111
|
+
is_coordinate : bool
|
|
112
|
+
True if both bonding electrons were donated by one atom
|
|
113
|
+
(coordinate / dative bond).
|
|
114
|
+
donor : str or None
|
|
115
|
+
For coordinate bonds, the symbol of the electron-pair donor.
|
|
116
|
+
"""
|
|
117
|
+
symbol_a: str
|
|
118
|
+
symbol_b: str
|
|
119
|
+
bond_order: int = 1
|
|
120
|
+
is_coordinate: bool = False
|
|
121
|
+
donor: str | None = None
|
|
122
|
+
|
|
123
|
+
def __post_init__(self):
|
|
124
|
+
if self.symbol_a not in SYMBOL_TO_Z:
|
|
125
|
+
raise ValueError(f"Unknown element: {self.symbol_a}")
|
|
126
|
+
if self.symbol_b not in SYMBOL_TO_Z:
|
|
127
|
+
raise ValueError(f"Unknown element: {self.symbol_b}")
|
|
128
|
+
if self.bond_order not in (1, 2, 3):
|
|
129
|
+
raise ValueError(f"Bond order must be 1, 2, or 3; got {self.bond_order}")
|
|
130
|
+
if self.is_coordinate and self.donor is None:
|
|
131
|
+
raise ValueError("Coordinate bond requires a donor symbol")
|
|
132
|
+
|
|
133
|
+
# --- Electronegativity & polarity ---
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def en_a(self) -> float:
|
|
137
|
+
return electronegativity(self.symbol_a)
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def en_b(self) -> float:
|
|
141
|
+
return electronegativity(self.symbol_b)
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def delta_en(self) -> float:
|
|
145
|
+
"""Absolute electronegativity difference (Pauling scale)."""
|
|
146
|
+
return abs(self.en_a - self.en_b)
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def polarity(self) -> BondPolarity:
|
|
150
|
+
"""Classify the bond by its electronegativity difference."""
|
|
151
|
+
d = self.delta_en
|
|
152
|
+
if d < NONPOLAR_THRESHOLD:
|
|
153
|
+
return BondPolarity.NONPOLAR_COVALENT
|
|
154
|
+
elif d < POLAR_COVALENT_MAX:
|
|
155
|
+
return BondPolarity.POLAR_COVALENT
|
|
156
|
+
else:
|
|
157
|
+
return BondPolarity.IONIC
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def partial_positive(self) -> str:
|
|
161
|
+
"""Symbol of the atom bearing partial positive charge (delta+)."""
|
|
162
|
+
if self.en_a <= self.en_b:
|
|
163
|
+
return self.symbol_a
|
|
164
|
+
return self.symbol_b
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def partial_negative(self) -> str:
|
|
168
|
+
"""Symbol of the atom bearing partial negative charge (delta-)."""
|
|
169
|
+
if self.en_a > self.en_b:
|
|
170
|
+
return self.symbol_a
|
|
171
|
+
return self.symbol_b
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def percent_ionic_character(self) -> float:
|
|
175
|
+
"""Estimate ionic character using Pauling's equation.
|
|
176
|
+
|
|
177
|
+
%ionic = 100 * (1 - exp(-0.25 * delta_EN^2))
|
|
178
|
+
"""
|
|
179
|
+
return 100.0 * (1.0 - math.exp(-0.25 * self.delta_en ** 2))
|
|
180
|
+
|
|
181
|
+
# --- Bond length ---
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def bond_length_pm(self) -> float:
|
|
185
|
+
"""Estimated bond length in picometres."""
|
|
186
|
+
return estimated_bond_length_pm(self.symbol_a, self.symbol_b,
|
|
187
|
+
self.bond_order)
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def bond_length_angstrom(self) -> float:
|
|
191
|
+
"""Estimated bond length in angstroms."""
|
|
192
|
+
return self.bond_length_pm / 100.0
|
|
193
|
+
|
|
194
|
+
# --- Bond dissociation energy ---
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def dissociation_energy_kj(self) -> float | None:
|
|
198
|
+
"""Mean bond dissociation energy in kJ/mol, or None if unknown."""
|
|
199
|
+
return BDE_TABLE.get(_bde_key(self.symbol_a, self.symbol_b, self.bond_order))
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def dissociation_energy_ev(self) -> float | None:
|
|
203
|
+
"""Mean bond dissociation energy in eV per bond, or None if unknown."""
|
|
204
|
+
e = self.dissociation_energy_kj
|
|
205
|
+
if e is None:
|
|
206
|
+
return None
|
|
207
|
+
return e / 96.485 # 1 eV = 96.485 kJ/mol
|
|
208
|
+
|
|
209
|
+
# --- Orbital composition ---
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def orbital_contributions(self) -> list[OrbitalContribution]:
|
|
213
|
+
"""Sigma and pi orbital contributions making up this bond."""
|
|
214
|
+
return sigma_pi_composition(self.bond_order)
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def sigma_bonds(self) -> int:
|
|
218
|
+
"""Number of sigma bonds (always 1 for a covalent bond)."""
|
|
219
|
+
return 1
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def pi_bonds(self) -> int:
|
|
223
|
+
"""Number of pi bonds (0, 1, or 2)."""
|
|
224
|
+
return self.bond_order - 1
|
|
225
|
+
|
|
226
|
+
# --- Dipole moment estimate ---
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def dipole_moment_debye(self) -> float:
|
|
230
|
+
"""Estimate bond dipole moment in Debye using the point-charge model.
|
|
231
|
+
|
|
232
|
+
mu = q * d * 4.8032
|
|
233
|
+
|
|
234
|
+
where q is the partial charge in electron units (percent_ionic / 100)
|
|
235
|
+
and d is the bond length in Angstroms. The constant 4.8032 converts
|
|
236
|
+
electron-Angstroms to Debye (1 D = 3.336e-30 C*m).
|
|
237
|
+
|
|
238
|
+
This is a simplified model. Real dipole moments depend on the full
|
|
239
|
+
3D electron distribution.
|
|
240
|
+
"""
|
|
241
|
+
if self.delta_en == 0.0:
|
|
242
|
+
return 0.0
|
|
243
|
+
partial_charge_e = self.percent_ionic_character / 100.0
|
|
244
|
+
return partial_charge_e * self.bond_length_angstrom * DEBYE_PER_E_ANGSTROM
|
|
245
|
+
|
|
246
|
+
# --- Display ---
|
|
247
|
+
|
|
248
|
+
@property
|
|
249
|
+
def order_symbol(self) -> str:
|
|
250
|
+
return {1: "-", 2: "=", 3: "#"}.get(self.bond_order, "?")
|
|
251
|
+
|
|
252
|
+
@property
|
|
253
|
+
def order_label(self) -> str:
|
|
254
|
+
return {1: "single", 2: "double", 3: "triple"}.get(self.bond_order, "?")
|
|
255
|
+
|
|
256
|
+
def __repr__(self):
|
|
257
|
+
tag = " (coordinate)" if self.is_coordinate else ""
|
|
258
|
+
return f"CovalentBond({self.symbol_a}{self.order_symbol}{self.symbol_b}, {self.order_label}{tag})"
|
|
259
|
+
|
|
260
|
+
def summary(self) -> str:
|
|
261
|
+
"""Return a multi-line human-readable summary of this bond."""
|
|
262
|
+
lines = [
|
|
263
|
+
f" {self.symbol_a}{self.order_symbol}{self.symbol_b} "
|
|
264
|
+
f"({self.order_label} bond)",
|
|
265
|
+
]
|
|
266
|
+
|
|
267
|
+
# Coordinate info
|
|
268
|
+
if self.is_coordinate:
|
|
269
|
+
lines.append(f" Type : coordinate (dative) bond")
|
|
270
|
+
lines.append(f" Donor : {self.donor}")
|
|
271
|
+
else:
|
|
272
|
+
lines.append(f" Type : standard covalent bond")
|
|
273
|
+
|
|
274
|
+
# Polarity
|
|
275
|
+
pol = self.polarity
|
|
276
|
+
pol_label = {
|
|
277
|
+
BondPolarity.NONPOLAR_COVALENT: "nonpolar covalent",
|
|
278
|
+
BondPolarity.POLAR_COVALENT: "polar covalent",
|
|
279
|
+
BondPolarity.IONIC: "ionic",
|
|
280
|
+
}[pol]
|
|
281
|
+
lines.append(f" Polarity : {pol_label} "
|
|
282
|
+
f"(delta EN = {self.delta_en:.2f})")
|
|
283
|
+
if pol == BondPolarity.POLAR_COVALENT:
|
|
284
|
+
lines.append(f" Dipole : "
|
|
285
|
+
f"delta+ on {self.partial_positive}, "
|
|
286
|
+
f"delta- on {self.partial_negative}")
|
|
287
|
+
lines.append(f" % ionic char : {self.percent_ionic_character:.1f}%")
|
|
288
|
+
lines.append(f" Dipole moment : ~{self.dipole_moment_debye:.2f} D")
|
|
289
|
+
|
|
290
|
+
# Geometry
|
|
291
|
+
lines.append(f" Bond length : {self.bond_length_pm:.0f} pm "
|
|
292
|
+
f"({self.bond_length_angstrom:.2f} A)")
|
|
293
|
+
|
|
294
|
+
# Energy
|
|
295
|
+
bde = self.dissociation_energy_kj
|
|
296
|
+
if bde is not None:
|
|
297
|
+
lines.append(f" BDE : {bde:.0f} kJ/mol "
|
|
298
|
+
f"({self.dissociation_energy_ev:.2f} eV)")
|
|
299
|
+
else:
|
|
300
|
+
lines.append(f" BDE : no reference data")
|
|
301
|
+
|
|
302
|
+
# Orbital decomposition
|
|
303
|
+
orb_strs = [repr(o) for o in self.orbital_contributions]
|
|
304
|
+
lines.append(f" Orbitals : {' + '.join(orb_strs)}")
|
|
305
|
+
|
|
306
|
+
return "\n".join(lines)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# ===================================================================
|
|
310
|
+
# Molecular polarity analysis
|
|
311
|
+
# ===================================================================
|
|
312
|
+
|
|
313
|
+
@dataclass
|
|
314
|
+
class MolecularBondAnalysis:
|
|
315
|
+
"""Analyse all covalent bonds in a molecule from its Lewis structure.
|
|
316
|
+
|
|
317
|
+
Constructs CovalentBond objects for each bond and determines whether
|
|
318
|
+
the molecule as a whole is likely polar or nonpolar based on geometry
|
|
319
|
+
symmetry and individual bond polarities.
|
|
320
|
+
|
|
321
|
+
Parameters
|
|
322
|
+
----------
|
|
323
|
+
formula : str
|
|
324
|
+
Molecular formula (e.g. 'H2O', 'CO2', 'CH4').
|
|
325
|
+
charge : int
|
|
326
|
+
Net charge on the species.
|
|
327
|
+
"""
|
|
328
|
+
formula: str
|
|
329
|
+
charge: int = 0
|
|
330
|
+
bonds: list[CovalentBond] = field(default_factory=list, init=False)
|
|
331
|
+
_lewis: object = field(default=None, init=False, repr=False)
|
|
332
|
+
|
|
333
|
+
def __post_init__(self):
|
|
334
|
+
from molbuilder.bonding.lewis import LewisStructure
|
|
335
|
+
self._lewis = LewisStructure(self.formula, self.charge)
|
|
336
|
+
self._build_bonds()
|
|
337
|
+
|
|
338
|
+
def _build_bonds(self):
|
|
339
|
+
"""Create CovalentBond objects from the Lewis structure."""
|
|
340
|
+
lew = self._lewis
|
|
341
|
+
for bond in lew.bonds:
|
|
342
|
+
sym_a = lew.atoms[bond.atom_a]
|
|
343
|
+
sym_b = lew.atoms[bond.atom_b]
|
|
344
|
+
self.bonds.append(CovalentBond(sym_a, sym_b, bond.order))
|
|
345
|
+
|
|
346
|
+
@property
|
|
347
|
+
def total_sigma_bonds(self) -> int:
|
|
348
|
+
return sum(b.sigma_bonds for b in self.bonds)
|
|
349
|
+
|
|
350
|
+
@property
|
|
351
|
+
def total_pi_bonds(self) -> int:
|
|
352
|
+
return sum(b.pi_bonds for b in self.bonds)
|
|
353
|
+
|
|
354
|
+
@property
|
|
355
|
+
def all_bonds_nonpolar(self) -> bool:
|
|
356
|
+
return all(b.polarity == BondPolarity.NONPOLAR_COVALENT
|
|
357
|
+
for b in self.bonds)
|
|
358
|
+
|
|
359
|
+
@property
|
|
360
|
+
def has_lone_pairs_on_central(self) -> bool:
|
|
361
|
+
return self._lewis.lone_pairs_on_central() > 0
|
|
362
|
+
|
|
363
|
+
@property
|
|
364
|
+
def is_symmetric(self) -> bool:
|
|
365
|
+
"""Heuristic: molecule is symmetric if all terminal atoms are
|
|
366
|
+
identical and the central atom has no lone pairs.
|
|
367
|
+
|
|
368
|
+
Diatomic molecules (A-B) are symmetric only if both atoms are
|
|
369
|
+
the same element (homonuclear).
|
|
370
|
+
"""
|
|
371
|
+
lew = self._lewis
|
|
372
|
+
if len(lew.atoms) <= 2:
|
|
373
|
+
# Homonuclear diatomic (H2, O2, N2) -> symmetric
|
|
374
|
+
# Heteronuclear diatomic (HCl, CO) -> not symmetric
|
|
375
|
+
return len(set(lew.atoms)) == 1
|
|
376
|
+
terminal_syms = {lew.atoms[i] for i in lew.terminal_indices}
|
|
377
|
+
orders = {b.bond_order for b in self.bonds}
|
|
378
|
+
return (len(terminal_syms) == 1
|
|
379
|
+
and len(orders) == 1
|
|
380
|
+
and not self.has_lone_pairs_on_central)
|
|
381
|
+
|
|
382
|
+
@property
|
|
383
|
+
def molecular_polarity(self) -> str:
|
|
384
|
+
"""Predict whether the molecule is polar or nonpolar.
|
|
385
|
+
|
|
386
|
+
A molecule is nonpolar if:
|
|
387
|
+
- all bonds are nonpolar, OR
|
|
388
|
+
- it has a symmetric geometry that cancels dipoles
|
|
389
|
+
Otherwise it is polar.
|
|
390
|
+
"""
|
|
391
|
+
if self.all_bonds_nonpolar:
|
|
392
|
+
return "nonpolar"
|
|
393
|
+
if self.is_symmetric:
|
|
394
|
+
return "nonpolar (symmetric -- dipoles cancel)"
|
|
395
|
+
return "polar"
|
|
396
|
+
|
|
397
|
+
def summary(self) -> str:
|
|
398
|
+
lines = [
|
|
399
|
+
f"{'=' * 60}",
|
|
400
|
+
f" Covalent Bond Analysis: {self.formula}"
|
|
401
|
+
+ (f" (charge {self.charge:+d})" if self.charge else ""),
|
|
402
|
+
f"{'=' * 60}",
|
|
403
|
+
]
|
|
404
|
+
for b in self.bonds:
|
|
405
|
+
lines.append(b.summary())
|
|
406
|
+
lines.append("")
|
|
407
|
+
|
|
408
|
+
lines.append(f" Total sigma bonds : {self.total_sigma_bonds}")
|
|
409
|
+
lines.append(f" Total pi bonds : {self.total_pi_bonds}")
|
|
410
|
+
lines.append(f" Molecular polarity: {self.molecular_polarity}")
|
|
411
|
+
lines.append(f"{'=' * 60}")
|
|
412
|
+
return "\n".join(lines)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
# ===================================================================
|
|
416
|
+
# Convenience constructors
|
|
417
|
+
# ===================================================================
|
|
418
|
+
|
|
419
|
+
def single_bond(sym_a: str, sym_b: str) -> CovalentBond:
|
|
420
|
+
"""Create a single covalent bond."""
|
|
421
|
+
return CovalentBond(sym_a, sym_b, bond_order=1)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def double_bond(sym_a: str, sym_b: str) -> CovalentBond:
|
|
425
|
+
"""Create a double covalent bond."""
|
|
426
|
+
return CovalentBond(sym_a, sym_b, bond_order=2)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def triple_bond(sym_a: str, sym_b: str) -> CovalentBond:
|
|
430
|
+
"""Create a triple covalent bond."""
|
|
431
|
+
return CovalentBond(sym_a, sym_b, bond_order=3)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def coordinate_bond(donor: str, acceptor: str,
|
|
435
|
+
bond_order: int = 1) -> CovalentBond:
|
|
436
|
+
"""Create a coordinate (dative) covalent bond.
|
|
437
|
+
|
|
438
|
+
In a coordinate bond both shared electrons originate from the donor.
|
|
439
|
+
Example: NH3 -> BF3 (N donates a lone pair to B).
|
|
440
|
+
"""
|
|
441
|
+
return CovalentBond(donor, acceptor, bond_order=bond_order,
|
|
442
|
+
is_coordinate=True, donor=donor)
|