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,6 @@
1
+ """Core data and utilities for molecular modeling."""
2
+ from molbuilder.core.constants import *
3
+ from molbuilder.core.elements import ELEMENTS, SYMBOL_TO_Z, from_symbol, from_name
4
+ from molbuilder.core.element_properties import electronegativity, covalent_radius_pm
5
+ from molbuilder.core.geometry import normalize, rotation_matrix, place_atom_zmatrix
6
+ from molbuilder.core.bond_data import bond_length, SP3_ANGLE, SP2_ANGLE, SP_ANGLE
@@ -0,0 +1,170 @@
1
+ """
2
+ Bond reference data: standard bond lengths, dissociation energies,
3
+ torsion barrier parameters.
4
+
5
+ Consolidated from molecular_conformations.py (STANDARD_BOND_LENGTHS,
6
+ TORSION_BARRIERS) and covalent_bonds.py (BDE_TABLE).
7
+ """
8
+
9
+ from molbuilder.core.element_properties import estimated_bond_length_angstrom
10
+
11
+ # ===================================================================
12
+ # Standard bond lengths in Angstroms (experimental averages)
13
+ # ===================================================================
14
+ # Keys: (symbol_a, symbol_b, order) with symbols in alphabetical order.
15
+
16
+ STANDARD_BOND_LENGTHS: dict[tuple[str, str, int], float] = {
17
+ ("C", "C", 1): 1.54,
18
+ ("C", "C", 2): 1.34,
19
+ ("C", "C", 3): 1.20,
20
+ ("C", "H", 1): 1.09,
21
+ ("C", "O", 1): 1.43,
22
+ ("C", "O", 2): 1.23,
23
+ ("C", "N", 1): 1.47,
24
+ ("C", "N", 2): 1.29,
25
+ ("C", "N", 3): 1.16,
26
+ ("C", "F", 1): 1.35,
27
+ ("C", "Cl", 1): 1.77,
28
+ ("Br", "C", 1): 1.94,
29
+ ("C", "I", 1): 2.14,
30
+ ("C", "S", 1): 1.82,
31
+ ("H", "O", 1): 0.96,
32
+ ("H", "N", 1): 1.01,
33
+ ("H", "S", 1): 1.34,
34
+ ("F", "F", 1): 1.42,
35
+ ("Cl", "Cl", 1): 1.99,
36
+ ("Br", "Br", 1): 2.28,
37
+ ("H", "H", 1): 0.74,
38
+ ("S", "S", 1): 2.05,
39
+ ("N", "O", 1): 1.36,
40
+ ("N", "O", 2): 1.21,
41
+ ("O", "P", 1): 1.63,
42
+ ("O", "P", 2): 1.48,
43
+ ("H", "P", 1): 1.44,
44
+ }
45
+
46
+ # Standard bond angles in degrees
47
+ SP3_ANGLE = 109.47 # tetrahedral
48
+ SP2_ANGLE = 120.0 # trigonal planar
49
+ SP_ANGLE = 180.0 # linear
50
+
51
+
52
+ # ===================================================================
53
+ # Bond dissociation energies (kJ/mol)
54
+ # ===================================================================
55
+ # Sources: CRC Handbook, Darwent (NSRDS-NBS 31), Kerr & Stocker.
56
+ # Keys: (symbol_a, symbol_b, bond_order) alphabetical order.
57
+
58
+ BDE_TABLE: dict[tuple[str, str, int], float] = {
59
+ # Single bonds
60
+ ("H", "H", 1): 436,
61
+ ("C", "H", 1): 413,
62
+ ("C", "C", 1): 348,
63
+ ("C", "N", 1): 293,
64
+ ("C", "O", 1): 358,
65
+ ("C", "F", 1): 485,
66
+ ("C", "Cl", 1): 328,
67
+ ("C", "Br", 1): 276,
68
+ ("C", "I", 1): 240,
69
+ ("C", "S", 1): 272,
70
+ ("N", "H", 1): 391,
71
+ ("N", "N", 1): 163,
72
+ ("N", "O", 1): 201,
73
+ ("O", "H", 1): 463,
74
+ ("O", "O", 1): 146,
75
+ ("F", "F", 1): 155,
76
+ ("F", "H", 1): 567,
77
+ ("Cl", "Cl", 1): 242,
78
+ ("Cl", "H", 1): 431,
79
+ ("Br", "Br", 1): 193,
80
+ ("Br", "H", 1): 366,
81
+ ("H", "I", 1): 297,
82
+ ("I", "I", 1): 151,
83
+ ("H", "S", 1): 363,
84
+ ("S", "S", 1): 266,
85
+ ("P", "H", 1): 322,
86
+ ("O", "Si", 1): 452,
87
+ ("H", "Si", 1): 318,
88
+ # Double bonds
89
+ ("C", "C", 2): 614,
90
+ ("C", "N", 2): 615,
91
+ ("C", "O", 2): 799,
92
+ ("N", "N", 2): 418,
93
+ ("O", "O", 2): 498,
94
+ ("N", "O", 2): 607,
95
+ ("C", "S", 2): 577,
96
+ ("S", "O", 2): 522,
97
+ # Additional single bonds (P2-DATA-2)
98
+ ("C", "P", 1): 264, # Luo 2007
99
+ ("C", "Si", 1): 318, # CRC Handbook
100
+ ("N", "S", 1): 272, # Benson estimates
101
+ ("Cl", "N", 1): 200, # chloramine / N-Cl
102
+ ("F", "O", 1): 190, # OF2
103
+ ("O", "S", 1): 265, # dimethyl sulfoxide (single)
104
+ ("F", "S", 1): 327, # SF6 type
105
+ ("N", "P", 1): 230, # P-N amine
106
+ ("F", "Si", 1): 565, # SiF4
107
+ # Triple bonds
108
+ ("C", "C", 3): 839,
109
+ ("C", "N", 3): 891,
110
+ ("N", "N", 3): 941,
111
+ ("C", "O", 3): 1072,
112
+ }
113
+
114
+
115
+ # ===================================================================
116
+ # Torsion barrier parameters (kJ/mol) -- OPLS-AA
117
+ # ===================================================================
118
+
119
+ TORSION_BARRIERS: dict[str, dict[str, float]] = {
120
+ # sp3-sp3 torsions (alkanes)
121
+ "H_sp3_sp3_H": {"V1": 0.0, "V2": 0.0, "V3": 1.39},
122
+ "C_sp3_sp3_C": {"V1": 2.73, "V2": -0.53, "V3": 0.84},
123
+ "C_sp3_sp3_H": {"V1": 0.0, "V2": 0.0, "V3": 0.76},
124
+ "H_sp3_sp3_C": {"V1": 0.0, "V2": 0.0, "V3": 0.76},
125
+ # sp2-sp3 torsions (P2-DATA-3, OPLS-AA Jorgensen et al. JACS 1996)
126
+ "C_sp2_sp3_C": {"V1": 0.0, "V2": 0.0, "V3": 0.50},
127
+ "C_sp2_sp3_H": {"V1": 0.0, "V2": 0.0, "V3": 0.50},
128
+ "H_sp2_sp3_C": {"V1": 0.0, "V2": 0.0, "V3": 0.50},
129
+ "H_sp2_sp3_H": {"V1": 0.0, "V2": 0.0, "V3": 0.50},
130
+ "H_sp3_sp2_C": {"V1": 0.0, "V2": 0.0, "V3": 0.50},
131
+ "C_sp3_sp2_C": {"V1": 0.0, "V2": 0.0, "V3": 0.50},
132
+ "C_sp3_sp2_H": {"V1": 0.0, "V2": 0.0, "V3": 0.50},
133
+ # sp2-sp2 torsions (conjugated systems)
134
+ "C_sp2_sp2_C": {"V1": 0.0, "V2": 12.0, "V3": 0.0},
135
+ "C_sp2_sp2_H": {"V1": 0.0, "V2": 12.0, "V3": 0.0},
136
+ "H_sp2_sp2_C": {"V1": 0.0, "V2": 12.0, "V3": 0.0},
137
+ "H_sp2_sp2_H": {"V1": 0.0, "V2": 12.0, "V3": 0.0},
138
+ "default": {"V1": 0.0, "V2": 0.0, "V3": 1.00},
139
+ }
140
+
141
+
142
+ # ===================================================================
143
+ # Electronegativity thresholds for bond polarity
144
+ # ===================================================================
145
+
146
+ NONPOLAR_THRESHOLD = 0.4
147
+ POLAR_COVALENT_MAX = 1.7
148
+
149
+
150
+ # ===================================================================
151
+ # Lookup functions
152
+ # ===================================================================
153
+
154
+ def bond_length(sym_a: str, sym_b: str, order: int = 1) -> float:
155
+ """Look up standard bond length in Angstroms.
156
+
157
+ Checks STANDARD_BOND_LENGTHS first, falls back to
158
+ element_properties covalent-radii estimate.
159
+ """
160
+ a, b = sorted([sym_a, sym_b])
161
+ key = (a, b, order)
162
+ if key in STANDARD_BOND_LENGTHS:
163
+ return STANDARD_BOND_LENGTHS[key]
164
+ return estimated_bond_length_angstrom(sym_a, sym_b, order)
165
+
166
+
167
+ def bde_lookup(sym_a: str, sym_b: str, order: int = 1) -> float | None:
168
+ """Look up mean bond dissociation energy in kJ/mol, or None."""
169
+ a, b = sorted([sym_a, sym_b])
170
+ return BDE_TABLE.get((a, b, order))
@@ -0,0 +1,51 @@
1
+ """
2
+ Physical constants (SI units) used throughout molbuilder.
3
+
4
+ Consolidated from bohr_model.py, quantum_wavefunctions.py, and
5
+ atomic_particle_physical_laws.py.
6
+ """
7
+
8
+ import math
9
+
10
+ # ---------------------------------------------------------------------------
11
+ # Fundamental constants
12
+ # ---------------------------------------------------------------------------
13
+ BOHR_RADIUS_M = 5.29177210903e-11 # metres
14
+ BOHR_RADIUS_PM = 52.9177210903 # picometres
15
+ PLANCK_CONSTANT = 6.62607015e-34 # J s
16
+ HBAR = PLANCK_CONSTANT / (2 * math.pi) # reduced Planck constant (J s)
17
+ SPEED_OF_LIGHT = 2.99792458e8 # m/s
18
+ ELECTRON_CHARGE = 1.602176634e-19 # C
19
+ ELECTRON_MASS = 9.1093837015e-31 # kg
20
+ COULOMB_CONSTANT = 8.9875517873681764e9 # N m^2 / C^2
21
+ VACUUM_PERMITTIVITY = 8.8541878128e-12 # F/m
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Unit conversions
25
+ # ---------------------------------------------------------------------------
26
+ EV_TO_JOULES = 1.602176634e-19
27
+ RYDBERG_ENERGY_EV = 13.605693122994 # eV
28
+ DEBYE_PER_E_ANGSTROM = 4.8032 # 1 e*A = 4.8032 D
29
+
30
+ # ---------------------------------------------------------------------------
31
+ # Fine structure constant
32
+ # ---------------------------------------------------------------------------
33
+ FINE_STRUCTURE_ALPHA = (
34
+ ELECTRON_CHARGE**2
35
+ / (4 * math.pi * VACUUM_PERMITTIVITY * HBAR * SPEED_OF_LIGHT)
36
+ )
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # Shell capacity
40
+ # ---------------------------------------------------------------------------
41
+ MAX_ELECTRONS_PER_SHELL = [2 * n**2 for n in range(1, 8)] # shells 1-7
42
+
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # Electrostatics
46
+ # ---------------------------------------------------------------------------
47
+ def coulombs_law(q1: float, q2: float, r: float) -> float:
48
+ """Coulomb force between two charges q1, q2 (Coulombs) separated by r (metres)."""
49
+ if r <= 0:
50
+ raise ValueError("Distance must be positive")
51
+ return COULOMB_CONSTANT * (abs(q1) * abs(q2)) / r**2
@@ -0,0 +1,183 @@
1
+ """
2
+ Chemical properties of elements: electronegativity, covalent radii,
3
+ CPK colours, octet targets, period/group information.
4
+
5
+ Migrated from element_data.py.
6
+ """
7
+
8
+ import warnings
9
+
10
+ from molbuilder.core.elements import SYMBOL_TO_Z
11
+
12
+ # ---------------------------------------------------------------------------
13
+ # Pauling electronegativity (symbol -> float or None)
14
+ # ---------------------------------------------------------------------------
15
+ PAULING_ELECTRONEGATIVITY: dict[str, float | None] = {
16
+ "H": 2.20, "He": None,
17
+ "Li": 0.98, "Be": 1.57, "B": 2.04, "C": 2.55, "N": 3.04,
18
+ "O": 3.44, "F": 3.98, "Ne": None,
19
+ "Na": 0.93, "Mg": 1.31, "Al": 1.61, "Si": 1.90, "P": 2.19,
20
+ "S": 2.58, "Cl": 3.16, "Ar": None,
21
+ "K": 0.82, "Ca": 1.00, "Sc": 1.36, "Ti": 1.54, "V": 1.63,
22
+ "Cr": 1.66, "Mn": 1.55, "Fe": 1.83, "Co": 1.88, "Ni": 1.91,
23
+ "Cu": 1.90, "Zn": 1.65, "Ga": 1.81, "Ge": 2.01, "As": 2.18,
24
+ "Se": 2.55, "Br": 2.96, "Kr": 3.00,
25
+ "Rb": 0.82, "Sr": 0.95, "Y": 1.22, "Zr": 1.33, "Nb": 1.60,
26
+ "Mo": 2.16, "Tc": 1.90, "Ru": 2.20, "Rh": 2.28, "Pd": 2.20,
27
+ "Ag": 1.93, "Cd": 1.69, "In": 1.78, "Sn": 1.96, "Sb": 2.05,
28
+ "Te": 2.10, "I": 2.66, "Xe": 2.60,
29
+ "Cs": 0.79, "Ba": 0.89, "La": 1.10, "Ce": 1.12, "Pr": 1.13,
30
+ "Nd": 1.14, "Pm": 1.13, "Sm": 1.17, "Eu": 1.20, "Gd": 1.20,
31
+ "Tb": 1.10, "Dy": 1.22, "Ho": 1.23, "Er": 1.24, "Tm": 1.25,
32
+ "Yb": 1.10, "Lu": 1.27, "Hf": 1.30, "Ta": 1.50, "W": 2.36,
33
+ "Re": 1.90, "Os": 2.20, "Ir": 2.20, "Pt": 2.28, "Au": 2.54,
34
+ "Hg": 2.00, "Tl": 1.62, "Pb": 1.87, "Bi": 2.02, "Po": 2.00,
35
+ "At": 2.20, "Rn": 2.20,
36
+ "Fr": 0.70, "Ra": 0.90, "Ac": 1.10, "Th": 1.30, "Pa": 1.50,
37
+ "U": 1.38, "Np": 1.36, "Pu": 1.28, "Am": 1.30, "Cm": 1.30,
38
+ "Bk": 1.30, "Cf": 1.30, "Es": 1.30, "Fm": 1.30, "Md": 1.30,
39
+ "No": 1.30, "Lr": 1.30,
40
+ "Rf": None, "Db": None, "Sg": None, "Bh": None, "Hs": None,
41
+ "Mt": None, "Ds": None, "Rg": None, "Cn": None, "Nh": None,
42
+ "Fl": None, "Mc": None, "Lv": None, "Ts": None, "Og": None,
43
+ }
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # Single-bond covalent radii in picometres (Cordero et al. 2008)
47
+ # ---------------------------------------------------------------------------
48
+ COVALENT_RADII_PM: dict[str, float] = {
49
+ "H": 31, "He": 28,
50
+ "Li": 128, "Be": 96, "B": 84, "C": 76, "N": 71,
51
+ "O": 66, "F": 57, "Ne": 58,
52
+ "Na": 166, "Mg": 141, "Al": 121, "Si": 111, "P": 107,
53
+ "S": 105, "Cl": 102, "Ar": 106,
54
+ "K": 203, "Ca": 176, "Sc": 170, "Ti": 160, "V": 153,
55
+ "Cr": 139, "Mn": 139, "Fe": 132, "Co": 126, "Ni": 124,
56
+ "Cu": 132, "Zn": 122, "Ga": 122, "Ge": 120, "As": 119,
57
+ "Se": 120, "Br": 120, "Kr": 116,
58
+ "Rb": 220, "Sr": 195, "Y": 190, "Zr": 175, "Nb": 164,
59
+ "Mo": 154, "Tc": 147, "Ru": 146, "Rh": 142, "Pd": 139,
60
+ "Ag": 145, "Cd": 144, "In": 142, "Sn": 139, "Sb": 139,
61
+ "Te": 138, "I": 139, "Xe": 140,
62
+ "Cs": 244, "Ba": 215, "La": 207, "Ce": 204, "Pr": 203,
63
+ "Nd": 201, "Pm": 199, "Sm": 198, "Eu": 198, "Gd": 196,
64
+ "Tb": 194, "Dy": 192, "Ho": 192, "Er": 189, "Tm": 190,
65
+ "Yb": 187, "Lu": 187, "Hf": 175, "Ta": 170, "W": 162,
66
+ "Re": 151, "Os": 144, "Ir": 141, "Pt": 136, "Au": 136,
67
+ "Hg": 132, "Tl": 145, "Pb": 146, "Bi": 148, "Po": 140,
68
+ "At": 150, "Rn": 150,
69
+ "Fr": 260, "Ra": 221, "Ac": 215, "Th": 206, "Pa": 200,
70
+ "U": 196, "Np": 190, "Pu": 187, "Am": 180, "Cm": 169,
71
+ # Elements 97-118 (Pyykko & Atsumi 2009, estimated for superheavy)
72
+ "Bk": 168, "Cf": 168, "Es": 165, "Fm": 167, "Md": 173,
73
+ "No": 176, "Lr": 161, "Rf": 157, "Db": 149, "Sg": 143,
74
+ "Bh": 141, "Hs": 134, "Mt": 129, "Ds": 128, "Rg": 121,
75
+ "Cn": 122, "Nh": 136, "Fl": 143, "Mc": 162, "Lv": 175,
76
+ "Ts": 165, "Og": 157,
77
+ }
78
+
79
+ # ---------------------------------------------------------------------------
80
+ # CPK colours (hex strings)
81
+ # ---------------------------------------------------------------------------
82
+ CPK_COLORS: dict[str, str] = {
83
+ "H": "#FFFFFF", "He": "#D9FFFF",
84
+ "Li": "#CC80FF", "Be": "#C2FF00", "B": "#FFB5B5", "C": "#909090",
85
+ "N": "#3050F8", "O": "#FF0D0D", "F": "#90E050", "Ne": "#B3E3F5",
86
+ "Na": "#AB5CF2", "Mg": "#8AFF00", "Al": "#BFA6A6", "Si": "#F0C8A0",
87
+ "P": "#FF8000", "S": "#FFFF30", "Cl": "#1FF01F", "Ar": "#80D1E3",
88
+ "K": "#8F40D4", "Ca": "#3DFF00", "Sc": "#E6E6E6", "Ti": "#BFC2C7",
89
+ "V": "#A6A6AB", "Cr": "#8A99C7", "Mn": "#9C7AC7", "Fe": "#E06633",
90
+ "Co": "#F090A0", "Ni": "#50D050", "Cu": "#C88033", "Zn": "#7D80B0",
91
+ "Ga": "#C28F8F", "Ge": "#668F8F", "As": "#BD80E3", "Se": "#FFA100",
92
+ "Br": "#A62929", "Kr": "#5CB8D1",
93
+ "Rb": "#702EB0", "Sr": "#00FF00", "Y": "#94FFFF", "Zr": "#94E0E0",
94
+ "Nb": "#73C2C9", "Mo": "#54B5B5", "Tc": "#3B9E9E", "Ru": "#248F8F",
95
+ "Rh": "#0A7D8C", "Pd": "#006985", "Ag": "#C0C0C0", "Cd": "#FFD98F",
96
+ "In": "#A67573", "Sn": "#668080", "Sb": "#9E63B5", "Te": "#D47A00",
97
+ "I": "#940094", "Xe": "#429EB0",
98
+ "default": "#FF69B4",
99
+ }
100
+
101
+ # ---------------------------------------------------------------------------
102
+ # Target electron counts (Lewis structure octet rules)
103
+ # ---------------------------------------------------------------------------
104
+ TARGET_ELECTRONS: dict[str, int] = {
105
+ "H": 2, "He": 2, "Li": 2, "Be": 4, "B": 6,
106
+ }
107
+
108
+
109
+ # ===================================================================
110
+ # Helper functions
111
+ # ===================================================================
112
+
113
+ def electronegativity(symbol: str) -> float:
114
+ """Pauling electronegativity. Returns 0.0 for elements without a value."""
115
+ val = PAULING_ELECTRONEGATIVITY.get(symbol)
116
+ return 0.0 if val is None else val
117
+
118
+
119
+ def covalent_radius_pm(symbol: str) -> float:
120
+ """Single-bond covalent radius in picometres. Fallback: 150 pm."""
121
+ val = COVALENT_RADII_PM.get(symbol)
122
+ if val is None:
123
+ warnings.warn(
124
+ f"No covalent radius data for '{symbol}'; using 150 pm fallback",
125
+ stacklevel=2,
126
+ )
127
+ return 150.0
128
+ return val
129
+
130
+
131
+ def estimated_bond_length_pm(symbol_a: str, symbol_b: str,
132
+ bond_order: int = 1) -> float:
133
+ """Estimate bond length in picometres from covalent radii.
134
+
135
+ Single bond: r(A) + r(B)
136
+ Double bond: ~0.87 * single
137
+ Triple bond: ~0.78 * single
138
+ """
139
+ single = covalent_radius_pm(symbol_a) + covalent_radius_pm(symbol_b)
140
+ if bond_order == 2:
141
+ return single * 0.87
142
+ elif bond_order == 3:
143
+ return single * 0.78
144
+ return single
145
+
146
+
147
+ def estimated_bond_length_angstrom(symbol_a: str, symbol_b: str,
148
+ bond_order: int = 1) -> float:
149
+ """Estimate bond length in Angstroms (pm / 100)."""
150
+ return estimated_bond_length_pm(symbol_a, symbol_b, bond_order) / 100.0
151
+
152
+
153
+ def cpk_color(symbol: str) -> str:
154
+ """CPK hex colour for an element, with fallback."""
155
+ return CPK_COLORS.get(symbol, CPK_COLORS["default"])
156
+
157
+
158
+ def target_electrons(symbol: str) -> int:
159
+ """Target electron count for octet/duet rule."""
160
+ return TARGET_ELECTRONS.get(symbol, 8)
161
+
162
+
163
+ def period(symbol: str) -> int:
164
+ """Period (row) of an element in the periodic table."""
165
+ z = SYMBOL_TO_Z.get(symbol, 0)
166
+ if z <= 2:
167
+ return 1
168
+ if z <= 10:
169
+ return 2
170
+ if z <= 18:
171
+ return 3
172
+ if z <= 36:
173
+ return 4
174
+ if z <= 54:
175
+ return 5
176
+ if z <= 86:
177
+ return 6
178
+ return 7
179
+
180
+
181
+ def can_expand_octet(symbol: str) -> bool:
182
+ """Whether an element can have an expanded octet (period 3+)."""
183
+ return period(symbol) >= 3
@@ -0,0 +1,181 @@
1
+ """
2
+ Periodic table element data: symbols, names, atomic weights.
3
+
4
+ Migrated from bohr_model.py -- the canonical element registry for molbuilder.
5
+ """
6
+
7
+ # ---------------------------------------------------------------------------
8
+ # Element data (atomic_number -> (symbol, name, standard_atomic_weight))
9
+ # ---------------------------------------------------------------------------
10
+ ELEMENTS: dict[int, tuple[str, str, float]] = {
11
+ 1: ("H", "Hydrogen", 1.008),
12
+ 2: ("He", "Helium", 4.003),
13
+ 3: ("Li", "Lithium", 6.941),
14
+ 4: ("Be", "Beryllium", 9.012),
15
+ 5: ("B", "Boron", 10.811),
16
+ 6: ("C", "Carbon", 12.011),
17
+ 7: ("N", "Nitrogen", 14.007),
18
+ 8: ("O", "Oxygen", 15.999),
19
+ 9: ("F", "Fluorine", 18.998),
20
+ 10: ("Ne", "Neon", 20.180),
21
+ 11: ("Na", "Sodium", 22.990),
22
+ 12: ("Mg", "Magnesium", 24.305),
23
+ 13: ("Al", "Aluminium", 26.982),
24
+ 14: ("Si", "Silicon", 28.086),
25
+ 15: ("P", "Phosphorus", 30.974),
26
+ 16: ("S", "Sulfur", 32.065),
27
+ 17: ("Cl", "Chlorine", 35.453),
28
+ 18: ("Ar", "Argon", 39.948),
29
+ 19: ("K", "Potassium", 39.098),
30
+ 20: ("Ca", "Calcium", 40.078),
31
+ 21: ("Sc", "Scandium", 44.956),
32
+ 22: ("Ti", "Titanium", 47.867),
33
+ 23: ("V", "Vanadium", 50.942),
34
+ 24: ("Cr", "Chromium", 51.996),
35
+ 25: ("Mn", "Manganese", 54.938),
36
+ 26: ("Fe", "Iron", 55.845),
37
+ 27: ("Co", "Cobalt", 58.933),
38
+ 28: ("Ni", "Nickel", 58.693),
39
+ 29: ("Cu", "Copper", 63.546),
40
+ 30: ("Zn", "Zinc", 65.380),
41
+ 31: ("Ga", "Gallium", 69.723),
42
+ 32: ("Ge", "Germanium", 72.630),
43
+ 33: ("As", "Arsenic", 74.922),
44
+ 34: ("Se", "Selenium", 78.971),
45
+ 35: ("Br", "Bromine", 79.904),
46
+ 36: ("Kr", "Krypton", 83.798),
47
+ 37: ("Rb", "Rubidium", 85.468),
48
+ 38: ("Sr", "Strontium", 87.620),
49
+ 39: ("Y", "Yttrium", 88.906),
50
+ 40: ("Zr", "Zirconium", 91.224),
51
+ 41: ("Nb", "Niobium", 92.906),
52
+ 42: ("Mo", "Molybdenum", 95.950),
53
+ 43: ("Tc", "Technetium", 98.000),
54
+ 44: ("Ru", "Ruthenium", 101.070),
55
+ 45: ("Rh", "Rhodium", 102.906),
56
+ 46: ("Pd", "Palladium", 106.420),
57
+ 47: ("Ag", "Silver", 107.868),
58
+ 48: ("Cd", "Cadmium", 112.414),
59
+ 49: ("In", "Indium", 114.818),
60
+ 50: ("Sn", "Tin", 118.710),
61
+ 51: ("Sb", "Antimony", 121.760),
62
+ 52: ("Te", "Tellurium", 127.600),
63
+ 53: ("I", "Iodine", 126.904),
64
+ 54: ("Xe", "Xenon", 131.293),
65
+ 55: ("Cs", "Caesium", 132.905),
66
+ 56: ("Ba", "Barium", 137.327),
67
+ 57: ("La", "Lanthanum", 138.905),
68
+ 58: ("Ce", "Cerium", 140.116),
69
+ 59: ("Pr", "Praseodymium",140.908),
70
+ 60: ("Nd", "Neodymium", 144.242),
71
+ 61: ("Pm", "Promethium", 145.000),
72
+ 62: ("Sm", "Samarium", 150.360),
73
+ 63: ("Eu", "Europium", 151.964),
74
+ 64: ("Gd", "Gadolinium", 157.250),
75
+ 65: ("Tb", "Terbium", 158.925),
76
+ 66: ("Dy", "Dysprosium", 162.500),
77
+ 67: ("Ho", "Holmium", 164.930),
78
+ 68: ("Er", "Erbium", 167.259),
79
+ 69: ("Tm", "Thulium", 168.934),
80
+ 70: ("Yb", "Ytterbium", 173.045),
81
+ 71: ("Lu", "Lutetium", 174.967),
82
+ 72: ("Hf", "Hafnium", 178.490),
83
+ 73: ("Ta", "Tantalum", 180.948),
84
+ 74: ("W", "Tungsten", 183.840),
85
+ 75: ("Re", "Rhenium", 186.207),
86
+ 76: ("Os", "Osmium", 190.230),
87
+ 77: ("Ir", "Iridium", 192.217),
88
+ 78: ("Pt", "Platinum", 195.084),
89
+ 79: ("Au", "Gold", 196.967),
90
+ 80: ("Hg", "Mercury", 200.592),
91
+ 81: ("Tl", "Thallium", 204.383),
92
+ 82: ("Pb", "Lead", 207.200),
93
+ 83: ("Bi", "Bismuth", 208.980),
94
+ 84: ("Po", "Polonium", 209.000),
95
+ 85: ("At", "Astatine", 210.000),
96
+ 86: ("Rn", "Radon", 222.000),
97
+ 87: ("Fr", "Francium", 223.000),
98
+ 88: ("Ra", "Radium", 226.000),
99
+ 89: ("Ac", "Actinium", 227.000),
100
+ 90: ("Th", "Thorium", 232.038),
101
+ 91: ("Pa", "Protactinium",231.036),
102
+ 92: ("U", "Uranium", 238.029),
103
+ 93: ("Np", "Neptunium", 237.000),
104
+ 94: ("Pu", "Plutonium", 244.000),
105
+ 95: ("Am", "Americium", 243.000),
106
+ 96: ("Cm", "Curium", 247.000),
107
+ 97: ("Bk", "Berkelium", 247.000),
108
+ 98: ("Cf", "Californium", 251.000),
109
+ 99: ("Es", "Einsteinium", 252.000),
110
+ 100:("Fm", "Fermium", 257.000),
111
+ 101:("Md", "Mendelevium", 258.000),
112
+ 102:("No", "Nobelium", 259.000),
113
+ 103:("Lr", "Lawrencium", 266.000),
114
+ 104:("Rf", "Rutherfordium",267.000),
115
+ 105:("Db", "Dubnium", 268.000),
116
+ 106:("Sg", "Seaborgium", 269.000),
117
+ 107:("Bh", "Bohrium", 270.000),
118
+ 108:("Hs", "Hassium", 277.000),
119
+ 109:("Mt", "Meitnerium", 278.000),
120
+ 110:("Ds", "Darmstadtium",281.000),
121
+ 111:("Rg", "Roentgenium", 282.000),
122
+ 112:("Cn", "Copernicium", 285.000),
123
+ 113:("Nh", "Nihonium", 286.000),
124
+ 114:("Fl", "Flerovium", 289.000),
125
+ 115:("Mc", "Moscovium", 290.000),
126
+ 116:("Lv", "Livermorium", 293.000),
127
+ 117:("Ts", "Tennessine", 294.000),
128
+ 118:("Og", "Oganesson", 294.000),
129
+ }
130
+
131
+ # Reverse lookup: symbol -> atomic number
132
+ SYMBOL_TO_Z: dict[str, int] = {v[0]: k for k, v in ELEMENTS.items()}
133
+
134
+ # Reverse lookup: lowercase name -> atomic number (O(1) name lookup)
135
+ _NAME_TO_Z: dict[str, int] = {v[1].lower(): k for k, v in ELEMENTS.items()}
136
+
137
+ # Noble gas core atomic numbers (for shorthand notation)
138
+ NOBLE_GASES: dict[int, str] = {
139
+ 2: "He", 10: "Ne", 18: "Ar",
140
+ 36: "Kr", 54: "Xe", 86: "Rn",
141
+ 118: "Og",
142
+ }
143
+
144
+
145
+ # ---------------------------------------------------------------------------
146
+ # Convenience lookups
147
+ # ---------------------------------------------------------------------------
148
+
149
+ def from_symbol(symbol: str) -> tuple[int, str, str, float]:
150
+ """Look up element by symbol. Returns (Z, symbol, name, weight)."""
151
+ sym = symbol.strip()
152
+ if len(sym) > 1:
153
+ sym = sym[0].upper() + sym[1:].lower()
154
+ else:
155
+ sym = sym.upper()
156
+ z = SYMBOL_TO_Z.get(sym)
157
+ if z is None:
158
+ raise ValueError(f"Unknown element symbol: {symbol}")
159
+ s, name, weight = ELEMENTS[z]
160
+ return z, s, name, weight
161
+
162
+
163
+ def from_name(name: str) -> tuple[int, str, str, float]:
164
+ """Look up element by name. Returns (Z, symbol, name, weight).
165
+
166
+ Uses a pre-built reverse mapping for O(1) lookup instead of linear scan.
167
+ """
168
+ name_lower = name.strip().lower()
169
+ z = _NAME_TO_Z.get(name_lower)
170
+ if z is None:
171
+ raise ValueError(f"Unknown element name: {name}")
172
+ sym, elem_name, weight = ELEMENTS[z]
173
+ return z, sym, elem_name, weight
174
+
175
+
176
+ def atomic_weight(symbol: str) -> float:
177
+ """Standard atomic weight for an element symbol."""
178
+ z = SYMBOL_TO_Z.get(symbol)
179
+ if z is None:
180
+ raise ValueError(f"Unknown element: {symbol}")
181
+ return ELEMENTS[z][2]