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,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)