react-cheminfo 0.1.0 → 0.3.0

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 (159) hide show
  1. package/README.md +50 -9
  2. package/lib/citation/core/formats.js +2 -3
  3. package/lib/citation/core/formats.js.map +1 -1
  4. package/lib/citation/core/segments.d.ts +8 -0
  5. package/lib/citation/core/segments.d.ts.map +1 -1
  6. package/lib/citation/core/segments.js +12 -3
  7. package/lib/citation/core/segments.js.map +1 -1
  8. package/lib/core.d.ts +1 -0
  9. package/lib/core.d.ts.map +1 -1
  10. package/lib/core.js +1 -0
  11. package/lib/core.js.map +1 -1
  12. package/lib/ecosystem/core/sites.d.ts +1 -1
  13. package/lib/ecosystem/core/sites.d.ts.map +1 -1
  14. package/lib/ecosystem/core/sites.js +45 -0
  15. package/lib/ecosystem/core/sites.js.map +1 -1
  16. package/lib/ecosystem/ui/EcosystemLinks.d.ts +33 -0
  17. package/lib/ecosystem/ui/EcosystemLinks.d.ts.map +1 -0
  18. package/lib/ecosystem/ui/EcosystemLinks.js +55 -0
  19. package/lib/ecosystem/ui/EcosystemLinks.js.map +1 -0
  20. package/lib/ecosystem/ui/EcosystemMenu.d.ts +3 -1
  21. package/lib/ecosystem/ui/EcosystemMenu.d.ts.map +1 -1
  22. package/lib/ecosystem/ui/EcosystemMenu.js +7 -84
  23. package/lib/ecosystem/ui/EcosystemMenu.js.map +1 -1
  24. package/lib/ecosystem/ui/SiteTile.d.ts +34 -0
  25. package/lib/ecosystem/ui/SiteTile.d.ts.map +1 -0
  26. package/lib/ecosystem/ui/SiteTile.js +78 -0
  27. package/lib/ecosystem/ui/SiteTile.js.map +1 -0
  28. package/lib/ecosystem/ui/glyphs.d.ts +16 -0
  29. package/lib/ecosystem/ui/glyphs.d.ts.map +1 -0
  30. package/lib/ecosystem/ui/glyphs.js +41 -0
  31. package/lib/ecosystem/ui/glyphs.js.map +1 -0
  32. package/lib/ecosystem/ui/index.d.ts +4 -0
  33. package/lib/ecosystem/ui/index.d.ts.map +1 -1
  34. package/lib/ecosystem/ui/index.js +2 -0
  35. package/lib/ecosystem/ui/index.js.map +1 -1
  36. package/lib/ecosystem/ui/marks.d.ts.map +1 -1
  37. package/lib/ecosystem/ui/marks.js +3 -28
  38. package/lib/ecosystem/ui/marks.js.map +1 -1
  39. package/lib/orbital/core/atomicGrid.d.ts +58 -0
  40. package/lib/orbital/core/atomicGrid.d.ts.map +1 -0
  41. package/lib/orbital/core/atomicGrid.js +69 -0
  42. package/lib/orbital/core/atomicGrid.js.map +1 -0
  43. package/lib/orbital/core/atomicOrbitals.d.ts +97 -0
  44. package/lib/orbital/core/atomicOrbitals.d.ts.map +1 -0
  45. package/lib/orbital/core/atomicOrbitals.js +135 -0
  46. package/lib/orbital/core/atomicOrbitals.js.map +1 -0
  47. package/lib/orbital/core/constants.d.ts +33 -0
  48. package/lib/orbital/core/constants.d.ts.map +1 -0
  49. package/lib/orbital/core/constants.js +21 -0
  50. package/lib/orbital/core/constants.js.map +1 -0
  51. package/lib/orbital/core/electronConfiguration.d.ts +106 -0
  52. package/lib/orbital/core/electronConfiguration.d.ts.map +1 -0
  53. package/lib/orbital/core/electronConfiguration.js +207 -0
  54. package/lib/orbital/core/electronConfiguration.js.map +1 -0
  55. package/lib/orbital/core/grid.d.ts +60 -0
  56. package/lib/orbital/core/grid.d.ts.map +1 -0
  57. package/lib/orbital/core/grid.js +81 -0
  58. package/lib/orbital/core/grid.js.map +1 -0
  59. package/lib/orbital/core/hydrogenic.d.ts +99 -0
  60. package/lib/orbital/core/hydrogenic.d.ts.map +1 -0
  61. package/lib/orbital/core/hydrogenic.js +181 -0
  62. package/lib/orbital/core/hydrogenic.js.map +1 -0
  63. package/lib/orbital/core/index.d.ts +22 -0
  64. package/lib/orbital/core/index.d.ts.map +1 -0
  65. package/lib/orbital/core/index.js +12 -0
  66. package/lib/orbital/core/index.js.map +1 -0
  67. package/lib/orbital/core/numerics.d.ts +39 -0
  68. package/lib/orbital/core/numerics.d.ts.map +1 -0
  69. package/lib/orbital/core/numerics.js +75 -0
  70. package/lib/orbital/core/numerics.js.map +1 -0
  71. package/lib/orbital/core/occupancy.d.ts +44 -0
  72. package/lib/orbital/core/occupancy.d.ts.map +1 -0
  73. package/lib/orbital/core/occupancy.js +85 -0
  74. package/lib/orbital/core/occupancy.js.map +1 -0
  75. package/lib/orbital/core/palette.d.ts +36 -0
  76. package/lib/orbital/core/palette.d.ts.map +1 -0
  77. package/lib/orbital/core/palette.js +37 -0
  78. package/lib/orbital/core/palette.js.map +1 -0
  79. package/lib/orbital/core/realHarmonics.d.ts +57 -0
  80. package/lib/orbital/core/realHarmonics.d.ts.map +1 -0
  81. package/lib/orbital/core/realHarmonics.js +144 -0
  82. package/lib/orbital/core/realHarmonics.js.map +1 -0
  83. package/lib/orbital/core/sample.d.ts +52 -0
  84. package/lib/orbital/core/sample.d.ts.map +1 -0
  85. package/lib/orbital/core/sample.js +54 -0
  86. package/lib/orbital/core/sample.js.map +1 -0
  87. package/lib/orbital/core/screening.d.ts +46 -0
  88. package/lib/orbital/core/screening.d.ts.map +1 -0
  89. package/lib/orbital/core/screening.js +72 -0
  90. package/lib/orbital/core/screening.js.map +1 -0
  91. package/lib/orbital/ui/AtomicOrbitalCanvas.d.ts +57 -0
  92. package/lib/orbital/ui/AtomicOrbitalCanvas.d.ts.map +1 -0
  93. package/lib/orbital/ui/AtomicOrbitalCanvas.js +105 -0
  94. package/lib/orbital/ui/AtomicOrbitalCanvas.js.map +1 -0
  95. package/lib/orbital/ui/AtomicOrbitalViewer.d.ts +73 -0
  96. package/lib/orbital/ui/AtomicOrbitalViewer.d.ts.map +1 -0
  97. package/lib/orbital/ui/AtomicOrbitalViewer.js +49 -0
  98. package/lib/orbital/ui/AtomicOrbitalViewer.js.map +1 -0
  99. package/lib/orbital/ui/camera.d.ts +36 -0
  100. package/lib/orbital/ui/camera.d.ts.map +1 -0
  101. package/lib/orbital/ui/camera.js +98 -0
  102. package/lib/orbital/ui/camera.js.map +1 -0
  103. package/lib/orbital/ui/capability.d.ts +28 -0
  104. package/lib/orbital/ui/capability.d.ts.map +1 -0
  105. package/lib/orbital/ui/capability.js +77 -0
  106. package/lib/orbital/ui/capability.js.map +1 -0
  107. package/lib/orbital/ui/index.d.ts +17 -0
  108. package/lib/orbital/ui/index.d.ts.map +1 -0
  109. package/lib/orbital/ui/index.js +13 -0
  110. package/lib/orbital/ui/index.js.map +1 -0
  111. package/lib/orbital/ui/renderVolume.d.ts +57 -0
  112. package/lib/orbital/ui/renderVolume.d.ts.map +1 -0
  113. package/lib/orbital/ui/renderVolume.js +120 -0
  114. package/lib/orbital/ui/renderVolume.js.map +1 -0
  115. package/lib/orbital/ui/viewer.d.ts +86 -0
  116. package/lib/orbital/ui/viewer.d.ts.map +1 -0
  117. package/lib/orbital/ui/viewer.js +155 -0
  118. package/lib/orbital/ui/viewer.js.map +1 -0
  119. package/lib/orbital/ui/volumeField.d.ts +49 -0
  120. package/lib/orbital/ui/volumeField.d.ts.map +1 -0
  121. package/lib/orbital/ui/volumeField.js +108 -0
  122. package/lib/orbital/ui/volumeField.js.map +1 -0
  123. package/lib/orbital.d.ts +11 -0
  124. package/lib/orbital.d.ts.map +1 -0
  125. package/lib/orbital.js +11 -0
  126. package/lib/orbital.js.map +1 -0
  127. package/package.json +13 -5
  128. package/src/citation/core/formats.ts +2 -3
  129. package/src/citation/core/segments.ts +11 -3
  130. package/src/core.ts +1 -0
  131. package/src/ecosystem/core/sites.ts +51 -1
  132. package/src/ecosystem/ui/EcosystemLinks.tsx +114 -0
  133. package/src/ecosystem/ui/EcosystemMenu.tsx +8 -135
  134. package/src/ecosystem/ui/SiteTile.tsx +148 -0
  135. package/src/ecosystem/ui/glyphs.tsx +246 -0
  136. package/src/ecosystem/ui/index.ts +4 -0
  137. package/src/ecosystem/ui/marks.tsx +5 -159
  138. package/src/orbital/core/atomicGrid.ts +115 -0
  139. package/src/orbital/core/atomicOrbitals.ts +212 -0
  140. package/src/orbital/core/constants.ts +36 -0
  141. package/src/orbital/core/electronConfiguration.ts +242 -0
  142. package/src/orbital/core/grid.ts +126 -0
  143. package/src/orbital/core/hydrogenic.ts +222 -0
  144. package/src/orbital/core/index.ts +72 -0
  145. package/src/orbital/core/numerics.ts +79 -0
  146. package/src/orbital/core/occupancy.ts +100 -0
  147. package/src/orbital/core/palette.ts +56 -0
  148. package/src/orbital/core/realHarmonics.ts +172 -0
  149. package/src/orbital/core/sample.ts +91 -0
  150. package/src/orbital/core/screening.ts +90 -0
  151. package/src/orbital/ui/AtomicOrbitalCanvas.tsx +181 -0
  152. package/src/orbital/ui/AtomicOrbitalViewer.tsx +150 -0
  153. package/src/orbital/ui/camera.ts +125 -0
  154. package/src/orbital/ui/capability.ts +102 -0
  155. package/src/orbital/ui/index.ts +17 -0
  156. package/src/orbital/ui/renderVolume.ts +190 -0
  157. package/src/orbital/ui/viewer.ts +186 -0
  158. package/src/orbital/ui/volumeField.ts +128 -0
  159. package/src/orbital.ts +11 -0
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Every orbital of one element, assembled from the four parts that describe it.
3
+ *
4
+ * The configuration says which subshells hold electrons, Slater's rules say how
5
+ * strongly each is bound, the hydrogen-like radial function gives it a size and
6
+ * its radial nodes, and a real spherical harmonic gives it a shape. This module
7
+ * is only the join — it computes nothing itself, which is what keeps each of
8
+ * those four testable on its own.
9
+ *
10
+ * Electrons inside a subshell are spread by Hund's rule: every orbital of the
11
+ * subshell takes one before any takes a second. That is why nitrogen shows
12
+ * three singly-occupied 2p orbitals and oxygen one pair and two singles.
13
+ */
14
+
15
+ import {
16
+ MADELUNG_ORDER,
17
+ configurationOf,
18
+ subshellLabel,
19
+ } from './electronConfiguration.ts';
20
+ import type { HydrogenicParameters } from './hydrogenic.ts';
21
+ import { meanRadius, orbitalEnergy, radialNodeCount } from './hydrogenic.ts';
22
+ import {
23
+ MINIMUM_CHARGE,
24
+ defaultMaximumShell,
25
+ hundDistribution,
26
+ outermostShell,
27
+ withoutOutermostElectron,
28
+ } from './occupancy.ts';
29
+ import type { RealHarmonic } from './realHarmonics.ts';
30
+ import { harmonicsOf, subshellLetter } from './realHarmonics.ts';
31
+ import { slaterScreening } from './screening.ts';
32
+
33
+ /** One atomic orbital of one element: a shape, a size and an occupancy. */
34
+ export interface AtomicOrbital {
35
+ /** Url-safe and unique within the element, e.g. `3dx2-y2`. */
36
+ id: string;
37
+ n: number;
38
+ /** Angular momentum quantum number: 0 = s, 1 = p, 2 = d, 3 = f. */
39
+ l: number;
40
+ /** Which of the `2ℓ + 1` real harmonics of the subshell. */
41
+ harmonic: RealHarmonic;
42
+ /** Subshell name without the subscript, e.g. `3d`. */
43
+ shell: string;
44
+ /** Electrons in this one orbital: 0, 1 or 2, after Hund's rule. */
45
+ electrons: number;
46
+ /** Electrons in the whole subshell. */
47
+ subshellElectrons: number;
48
+ /** Effective nuclear charge from Slater's rules. */
49
+ effectiveCharge: number;
50
+ /** Shielding `S` those rules produced. */
51
+ shielding: number;
52
+ /** `−13.6 Z_eff²/n²`, electronvolts. */
53
+ energy: number;
54
+ /** `n − ℓ − 1` spheres on which the wavefunction changes sign. */
55
+ radialNodes: number;
56
+ /** ℓ planes or cones on which it changes sign. */
57
+ angularNodes: number;
58
+ /** `⟨r⟩`, ångström. */
59
+ meanRadius: number;
60
+ /** True when the subshell is empty in the ground state. */
61
+ isVirtual: boolean;
62
+ /** True when this is the outermost occupied shell. */
63
+ isValence: boolean;
64
+ }
65
+
66
+ /** Options for {@link atomicOrbitalsOf}. */
67
+ export interface AtomicOrbitalOptions {
68
+ /**
69
+ * Highest principal quantum number to include. Defaults to one shell past
70
+ * the outermost occupied one, and never below 4 — a hydrogen atom whose list
71
+ * stopped at 2p would be missing exactly the 3d and 4f pictures it is famous
72
+ * for.
73
+ * @default max(4, outermost occupied n + 1), capped at 7
74
+ */
75
+ maximumShell?: number;
76
+ }
77
+
78
+ /**
79
+ * Every orbital of an element, in Madelung (roughly energetic) order.
80
+ * @param atomicNumber - Proton count, 1 to 118.
81
+ * @param options - See {@link AtomicOrbitalOptions}.
82
+ * @returns The orbitals, occupied ones first in filling order and the empty
83
+ * ones after them in the same order.
84
+ * @throws {Error} When the atomic number is outside the periodic table.
85
+ */
86
+ export function atomicOrbitalsOf(
87
+ atomicNumber: number,
88
+ options: AtomicOrbitalOptions = {},
89
+ ): AtomicOrbital[] {
90
+ const configuration = configurationOf(atomicNumber);
91
+ const valenceShell = outermostShell(configuration);
92
+ const { maximumShell = defaultMaximumShell(valenceShell) } = options;
93
+ const occupancies = new Map<string, number>();
94
+ for (const entry of configuration) {
95
+ occupancies.set(`${entry.n}.${entry.l}`, entry.electrons);
96
+ }
97
+ const promoted = withoutOutermostElectron(configuration);
98
+
99
+ const orbitals: AtomicOrbital[] = [];
100
+ for (const subshell of MADELUNG_ORDER) {
101
+ if (subshell.n > maximumShell) continue;
102
+ const subshellElectrons =
103
+ occupancies.get(`${subshell.n}.${subshell.l}`) ?? 0;
104
+ // An empty orbital is the one an electron would be *promoted* into, so the
105
+ // electron doing the promoting is not also left behind to screen it. This
106
+ // is what makes hydrogen's 3s and 4f come out as the exact hydrogen
107
+ // orbitals every textbook draws, rather than as unbound ones: screened by
108
+ // its own single electron, hydrogen's 3s would see a nuclear charge of zero.
109
+ const screening = slaterScreening(
110
+ atomicNumber,
111
+ subshellElectrons === 0 ? promoted : configuration,
112
+ subshell,
113
+ );
114
+ // Slater's rules can still over-screen a far virtual orbital; a charge at
115
+ // or below zero has no bound radial function at all.
116
+ const charge = Math.max(screening.effectiveCharge, MINIMUM_CHARGE);
117
+ const parameters: HydrogenicParameters = {
118
+ n: subshell.n,
119
+ l: subshell.l,
120
+ charge,
121
+ };
122
+ const harmonics = harmonicsOf(subshell.l);
123
+ const spread = hundDistribution(subshellElectrons, harmonics.length);
124
+ for (let index = 0; index < harmonics.length; index++) {
125
+ const harmonic = harmonics[index] as RealHarmonic;
126
+ orbitals.push({
127
+ id: orbitalId(subshell.n, subshell.l, harmonic),
128
+ n: subshell.n,
129
+ l: subshell.l,
130
+ harmonic,
131
+ shell: subshellLabel(subshell),
132
+ electrons: spread[index] as number,
133
+ subshellElectrons,
134
+ effectiveCharge: charge,
135
+ shielding: screening.shielding,
136
+ energy: orbitalEnergy(parameters),
137
+ radialNodes: radialNodeCount(parameters),
138
+ angularNodes: subshell.l,
139
+ meanRadius: meanRadius(parameters),
140
+ isVirtual: subshellElectrons === 0,
141
+ isValence: subshellElectrons > 0 && subshell.n === valenceShell,
142
+ });
143
+ }
144
+ }
145
+ return orbitals;
146
+ }
147
+
148
+ /**
149
+ * Find one orbital of an element by its id.
150
+ * @param orbitals - What {@link atomicOrbitalsOf} returned.
151
+ * @param id - Orbital id, e.g. `2px`.
152
+ * @returns The orbital, or `null` when the id names none.
153
+ */
154
+ export function findAtomicOrbital(
155
+ orbitals: readonly AtomicOrbital[],
156
+ id: string,
157
+ ): AtomicOrbital | null {
158
+ for (const orbital of orbitals) {
159
+ if (orbital.id === id) return orbital;
160
+ }
161
+ return null;
162
+ }
163
+
164
+ /**
165
+ * The hydrogen-like parameters an orbital is drawn from.
166
+ * @param orbital - The orbital.
167
+ * @returns Its principal quantum number, ℓ and effective charge.
168
+ */
169
+ export function hydrogenicParametersOf(
170
+ orbital: AtomicOrbital,
171
+ ): HydrogenicParameters {
172
+ return { n: orbital.n, l: orbital.l, charge: orbital.effectiveCharge };
173
+ }
174
+
175
+ /**
176
+ * The id of one orbital, as it appears in a URL.
177
+ * @param n - Principal quantum number.
178
+ * @param l - Angular momentum quantum number.
179
+ * @param harmonic - Which real harmonic of the subshell.
180
+ * @returns The id, e.g. `4fxyz`.
181
+ */
182
+ export function orbitalId(
183
+ n: number,
184
+ l: number,
185
+ harmonic: RealHarmonic,
186
+ ): string {
187
+ return `${n}${subshellLetter(l)}${harmonic.key}`;
188
+ }
189
+
190
+ /**
191
+ * The orbital a student should land on when they pick an element: the first
192
+ * one of the outermost occupied subshell, which is what the element's chemistry
193
+ * is about.
194
+ * @param orbitals - What {@link atomicOrbitalsOf} returned.
195
+ * @returns Its id, or `null` when the list is empty.
196
+ */
197
+ export function defaultOrbitalId(
198
+ orbitals: readonly AtomicOrbital[],
199
+ ): string | null {
200
+ let candidate: AtomicOrbital | null = null;
201
+ for (const orbital of orbitals) {
202
+ if (orbital.electrons === 0) continue;
203
+ if (candidate === null) {
204
+ candidate = orbital;
205
+ } else if (orbital.n > candidate.n) {
206
+ candidate = orbital;
207
+ } else if (orbital.n === candidate.n && orbital.l > candidate.l) {
208
+ candidate = orbital;
209
+ }
210
+ }
211
+ return (candidate ?? orbitals[0])?.id ?? null;
212
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The two physical constants the orbital maths is written in, and the units
3
+ * everything else here follows.
4
+ *
5
+ * Distances are **ångström** and amplitudes **Å^(-3/2)** throughout, so
6
+ * `∫|ψ|² dV` over a grid measured in ångström is one and nothing has to be
7
+ * converted on the way to a renderer. The Bohr radius therefore appears exactly
8
+ * once, in `hydrogenic.ts`.
9
+ */
10
+
11
+ /** The Bohr radius in ångström (CODATA 2018). */
12
+ export const BOHR_IN_ANGSTROM = 0.529_177_210_903;
13
+
14
+ /** One rydberg in electronvolts (CODATA 2018). */
15
+ export const RYDBERG_ELECTRONVOLTS = 13.605_693_122_994;
16
+
17
+ /**
18
+ * Share of an orbital's weight the default isosurface encloses.
19
+ *
20
+ * 0.85 is the contour a textbook draws: tight enough that the lobes are
21
+ * separate shapes, loose enough that the outer one of a 3s is still there.
22
+ */
23
+ export const ENCLOSED_WEIGHT = 0.85;
24
+
25
+ /**
26
+ * A point in space, ångström.
27
+ *
28
+ * Declared here rather than taken from a geometry package: three numbers are
29
+ * not worth a dependency, and this one crosses a worker boundary as a plain
30
+ * object.
31
+ */
32
+ export interface Vec3 {
33
+ x: number;
34
+ y: number;
35
+ z: number;
36
+ }
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Ground-state electron configurations, by the Aufbau principle and the twenty
3
+ * elements that ignore it.
4
+ *
5
+ * Subshells fill in Madelung order — increasing `n + ℓ`, and increasing `n`
6
+ * within a tie — which is why 4s fills before 3d. That rule gets 98 of the 118
7
+ * elements right; the rest are measured, not derived, and live in
8
+ * {@link ELEMENT_ANOMALIES}. Chromium is the canonical one: Aufbau predicts
9
+ * `[Ar]3d⁴4s²` and the atom is `[Ar]3d⁵4s¹`.
10
+ *
11
+ * Configurations are returned sorted by `n` then `ℓ`, the order a textbook
12
+ * writes them in — `[Ar]3d⁵4s¹`, not `[Ar]4s¹3d⁵`.
13
+ */
14
+
15
+ import { subshellLetter } from './realHarmonics.ts';
16
+
17
+ /** One subshell, e.g. `{ n: 3, l: 2 }` for 3d. */
18
+ export interface Subshell {
19
+ n: number;
20
+ /** Angular momentum quantum number: 0 = s, 1 = p, 2 = d, 3 = f. */
21
+ l: number;
22
+ }
23
+
24
+ /** A subshell and how many electrons sit in it. */
25
+ export interface SubshellOccupancy extends Subshell {
26
+ electrons: number;
27
+ }
28
+
29
+ /** No known element occupies a shell above n = 7. */
30
+ const MAXIMUM_PRINCIPAL_NUMBER = 7;
31
+
32
+ /** Protons in the heaviest element that has been made. */
33
+ export const HIGHEST_ATOMIC_NUMBER = 118;
34
+
35
+ /** The atomic numbers of the noble gases, which a configuration abbreviates on. */
36
+ export const NOBLE_GASES: readonly number[] = [2, 10, 18, 36, 54, 86, 118];
37
+
38
+ /**
39
+ * Reject an atomic number outside the periodic table.
40
+ * @param atomicNumber - Proton count to check.
41
+ * @throws {Error} When it is not an integer between 1 and 118.
42
+ */
43
+ export function assertAtomicNumber(atomicNumber: number): void {
44
+ if (
45
+ !Number.isInteger(atomicNumber) ||
46
+ atomicNumber < 1 ||
47
+ atomicNumber > HIGHEST_ATOMIC_NUMBER
48
+ ) {
49
+ throw new RangeError(`no element with atomic number ${atomicNumber}`);
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Subshells in the order they fill, `s` to `f` and up to n = 7 — everything the
55
+ * 118 known elements use, plus the empty subshells a light atom's virtual
56
+ * orbitals come from.
57
+ */
58
+ export const MADELUNG_ORDER: Subshell[] = buildMadelungOrder();
59
+
60
+ /**
61
+ * How many electrons a subshell holds: `2(2ℓ + 1)`.
62
+ * @param l - Angular momentum quantum number.
63
+ * @returns The capacity, `2(2ℓ + 1)`.
64
+ */
65
+ export function subshellCapacity(l: number): number {
66
+ return 2 * (2 * l + 1);
67
+ }
68
+
69
+ /**
70
+ * The measured ground states that Madelung order gets wrong, as the occupancy
71
+ * beyond the preceding noble gas.
72
+ *
73
+ * Half-filled and filled d shells are unusually stable, which is the story for
74
+ * Cr, Cu, Mo, Ag, Au and Gd; the early actinides are relativistic and 6d/5f sit
75
+ * almost on top of each other. Configurations past lawrencium are calculated
76
+ * rather than observed, so nothing beyond Z = 103 is listed here.
77
+ */
78
+ export const ELEMENT_ANOMALIES: Record<number, SubshellOccupancy[]> = {
79
+ 24: [o(3, 2, 5), o(4, 0, 1)],
80
+ 29: [o(3, 2, 10), o(4, 0, 1)],
81
+ 41: [o(4, 2, 4), o(5, 0, 1)],
82
+ 42: [o(4, 2, 5), o(5, 0, 1)],
83
+ 44: [o(4, 2, 7), o(5, 0, 1)],
84
+ 45: [o(4, 2, 8), o(5, 0, 1)],
85
+ 46: [o(4, 2, 10)],
86
+ 47: [o(4, 2, 10), o(5, 0, 1)],
87
+ 57: [o(5, 2, 1), o(6, 0, 2)],
88
+ 58: [o(4, 3, 1), o(5, 2, 1), o(6, 0, 2)],
89
+ 64: [o(4, 3, 7), o(5, 2, 1), o(6, 0, 2)],
90
+ 78: [o(4, 3, 14), o(5, 2, 9), o(6, 0, 1)],
91
+ 79: [o(4, 3, 14), o(5, 2, 10), o(6, 0, 1)],
92
+ 89: [o(6, 2, 1), o(7, 0, 2)],
93
+ 90: [o(6, 2, 2), o(7, 0, 2)],
94
+ 91: [o(5, 3, 2), o(6, 2, 1), o(7, 0, 2)],
95
+ 92: [o(5, 3, 3), o(6, 2, 1), o(7, 0, 2)],
96
+ 93: [o(5, 3, 4), o(6, 2, 1), o(7, 0, 2)],
97
+ 96: [o(5, 3, 7), o(6, 2, 1), o(7, 0, 2)],
98
+ 103: [o(5, 3, 14), o(7, 0, 2), o(7, 1, 1)],
99
+ };
100
+
101
+ /**
102
+ * The ground-state configuration of a neutral atom.
103
+ * @param atomicNumber - Proton count, 1 to 118.
104
+ * @returns Its occupied subshells, sorted by `n` then `ℓ`.
105
+ * @throws {Error} When the atomic number is outside the table.
106
+ */
107
+ export function configurationOf(atomicNumber: number): SubshellOccupancy[] {
108
+ assertAtomicNumber(atomicNumber);
109
+ const anomaly = ELEMENT_ANOMALIES[atomicNumber];
110
+ if (anomaly === undefined) return sortByShell(aufbauFill(atomicNumber));
111
+ const core = coreAtomicNumber(atomicNumber);
112
+ return sortByShell([
113
+ ...aufbauFill(core),
114
+ ...anomaly.map((entry) => ({ ...entry })),
115
+ ]);
116
+ }
117
+
118
+ /**
119
+ * Whether an element's ground state departs from Madelung order.
120
+ * @param atomicNumber - Proton count.
121
+ * @returns True for the twenty listed in {@link ELEMENT_ANOMALIES}.
122
+ */
123
+ export function isAnomalous(atomicNumber: number): boolean {
124
+ return atomicNumber in ELEMENT_ANOMALIES;
125
+ }
126
+
127
+ /**
128
+ * What Madelung order alone predicts, so the UI can show the two side by side.
129
+ * @param atomicNumber - Proton count, 1 to 118.
130
+ * @returns The predicted occupied subshells, sorted by `n` then `ℓ`.
131
+ */
132
+ export function aufbauConfigurationOf(
133
+ atomicNumber: number,
134
+ ): SubshellOccupancy[] {
135
+ return sortByShell(aufbauFill(atomicNumber));
136
+ }
137
+
138
+ /**
139
+ * The noble gas a configuration is abbreviated against.
140
+ * @param atomicNumber - Proton count.
141
+ * @returns The largest noble gas strictly below it, or 0 for hydrogen and
142
+ * helium, which are written out in full.
143
+ */
144
+ export function coreAtomicNumber(atomicNumber: number): number {
145
+ let core = 0;
146
+ for (const noble of NOBLE_GASES) {
147
+ if (noble >= atomicNumber) break;
148
+ core = noble;
149
+ }
150
+ return core;
151
+ }
152
+
153
+ /**
154
+ * Write a configuration the way a textbook does.
155
+ * @param occupancies - Subshells to write, in the order they should appear.
156
+ * @returns A string such as `1s² 2s² 2p⁶`.
157
+ */
158
+ export function formatConfiguration(
159
+ occupancies: readonly SubshellOccupancy[],
160
+ ): string {
161
+ const parts: string[] = [];
162
+ for (const occupancy of occupancies) parts.push(formatOccupancy(occupancy));
163
+ return parts.join(' ');
164
+ }
165
+
166
+ /**
167
+ * Write one subshell, e.g. `3d⁵`.
168
+ * @param occupancy - The subshell and its electron count.
169
+ * @returns The label with a superscript electron count.
170
+ */
171
+ export function formatOccupancy(occupancy: SubshellOccupancy): string {
172
+ return `${subshellLabel(occupancy)}${superscript(occupancy.electrons)}`;
173
+ }
174
+
175
+ /**
176
+ * Write one subshell without its electron count, e.g. `3d`.
177
+ * @param subshell - The subshell.
178
+ * @returns The principal quantum number followed by the subshell letter.
179
+ */
180
+ export function subshellLabel(subshell: Subshell): string {
181
+ return `${subshell.n}${subshellLetter(subshell.l)}`;
182
+ }
183
+
184
+ /**
185
+ * Render a number in unicode superscript digits.
186
+ * @param value - A non-negative integer.
187
+ * @returns The digits as superscripts, e.g. `14` becomes `¹⁴`.
188
+ */
189
+ export function superscript(value: number): string {
190
+ let text = '';
191
+ for (const digit of String(value)) {
192
+ text += SUPERSCRIPT_DIGITS[Number(digit)] ?? digit;
193
+ }
194
+ return text;
195
+ }
196
+
197
+ const SUPERSCRIPT_DIGITS = ['⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹'];
198
+
199
+ function o(n: number, l: number, electrons: number): SubshellOccupancy {
200
+ return { n, l, electrons };
201
+ }
202
+
203
+ /**
204
+ * Fill `count` electrons into {@link MADELUNG_ORDER}, lowest subshell first.
205
+ * @param count - Electrons to place.
206
+ * @returns The filled subshells, in Madelung order.
207
+ */
208
+ function aufbauFill(count: number): SubshellOccupancy[] {
209
+ const filled: SubshellOccupancy[] = [];
210
+ let left = count;
211
+ for (const subshell of MADELUNG_ORDER) {
212
+ if (left <= 0) break;
213
+ const electrons = Math.min(left, subshellCapacity(subshell.l));
214
+ filled.push({ n: subshell.n, l: subshell.l, electrons });
215
+ left -= electrons;
216
+ }
217
+ return filled;
218
+ }
219
+
220
+ function sortByShell(occupancies: SubshellOccupancy[]): SubshellOccupancy[] {
221
+ return occupancies.toSorted((first, second) =>
222
+ first.n === second.n ? first.l - second.l : first.n - second.n,
223
+ );
224
+ }
225
+
226
+ /**
227
+ * Madelung order: ascending `n + ℓ`, then ascending `n`. Capped at ℓ = 3 — the
228
+ * g subshells a Madelung diagram shows after 8s are unoccupied in every known
229
+ * element, and this site has no g harmonics to draw them with.
230
+ * @returns The subshells, in filling order.
231
+ */
232
+ function buildMadelungOrder(): Subshell[] {
233
+ const order: Subshell[] = [];
234
+ for (let sum = 1; sum <= 10; sum++) {
235
+ for (let l = Math.min(3, Math.ceil(sum / 2) - 1); l >= 0; l--) {
236
+ const n = sum - l;
237
+ if (n > MAXIMUM_PRINCIPAL_NUMBER || n <= l) continue;
238
+ order.push({ n, l });
239
+ }
240
+ }
241
+ return order;
242
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Sampling a wavefunction onto a dense scalar field for the isosurface renderer.
3
+ *
4
+ * The memory order is a hard contract with molstar: `Tensor.Space([nx, ny, nz],
5
+ * [0, 1, 2])` reads `dataOffset = x·ny·nz + y·nz + z`, so **z varies fastest**.
6
+ * The loops below are written in exactly that order and
7
+ * {@link gridIndex} is the single place the arithmetic is spelled out.
8
+ */
9
+
10
+ import type { Vec3 } from './constants.ts';
11
+
12
+ /** An axis-aligned sampling box, ångström. */
13
+ export interface GridBox {
14
+ /** Lowest corner. */
15
+ origin: Vec3;
16
+ /** Edge lengths along x, y and z. */
17
+ size: Vec3;
18
+ }
19
+
20
+ /**
21
+ * A wavefunction, sampled at one point.
22
+ * @param x - Offset from the origin along x, ångström.
23
+ * @param y - Offset along y.
24
+ * @param z - Offset along z.
25
+ * @returns The amplitude, Å^(-3/2).
26
+ */
27
+ export type OrbitalEvaluator = (x: number, y: number, z: number) => number;
28
+
29
+ /** A sampled scalar field plus the statistics an isovalue is chosen from. */
30
+ export interface OrbitalGrid {
31
+ /** `nx·ny·nz` samples, z varying fastest. */
32
+ data: Float32Array;
33
+ /** `[nx, ny, nz]`. */
34
+ dimensions: [number, number, number];
35
+ /** Position of sample `(0, 0, 0)`, ångström. */
36
+ origin: Vec3;
37
+ /** Distance between neighbouring samples on every axis, ångström. */
38
+ spacing: number;
39
+ min: number;
40
+ max: number;
41
+ mean: number;
42
+ /** Population standard deviation of the samples. */
43
+ sigma: number;
44
+ }
45
+
46
+ /**
47
+ * Sample a wavefunction over a box.
48
+ * @param evaluate - the wavefunction, cartesian ångström in, amplitude out.
49
+ * @param box - the region to cover.
50
+ * @param resolution - samples along the longest edge; the spacing it implies is
51
+ * used on all three axes, so the voxels stay cubic.
52
+ * @returns the field, its layout and its single-pass statistics.
53
+ * @throws {Error} when `resolution` is below 2 or the box has no volume.
54
+ */
55
+ export function evaluateGrid(
56
+ evaluate: OrbitalEvaluator,
57
+ box: GridBox,
58
+ resolution: number,
59
+ ): OrbitalGrid {
60
+ if (resolution < 2) {
61
+ throw new Error('a grid needs at least 2 samples per axis');
62
+ }
63
+ const { origin, size } = box;
64
+ const longestEdge = Math.max(size.x, size.y, size.z);
65
+ if (!(longestEdge > 0)) {
66
+ throw new Error('the sampling box must have a non-zero edge');
67
+ }
68
+ const spacing = longestEdge / (resolution - 1);
69
+ const countX = axisCount(size.x, spacing);
70
+ const countY = axisCount(size.y, spacing);
71
+ const countZ = axisCount(size.z, spacing);
72
+ const data = new Float32Array(countX * countY * countZ);
73
+ let min = Infinity;
74
+ let max = -Infinity;
75
+ let sum = 0;
76
+ let sumSquares = 0;
77
+ let index = 0;
78
+ for (let indexX = 0; indexX < countX; indexX++) {
79
+ const x = origin.x + indexX * spacing;
80
+ for (let indexY = 0; indexY < countY; indexY++) {
81
+ const y = origin.y + indexY * spacing;
82
+ for (let indexZ = 0; indexZ < countZ; indexZ++) {
83
+ const value = Math.fround(evaluate(x, y, origin.z + indexZ * spacing));
84
+ data[index++] = value;
85
+ if (value < min) min = value;
86
+ if (value > max) max = value;
87
+ sum += value;
88
+ sumSquares += value * value;
89
+ }
90
+ }
91
+ }
92
+ const count = data.length;
93
+ const mean = sum / count;
94
+ const variance = sumSquares / count - mean * mean;
95
+ return {
96
+ data,
97
+ dimensions: [countX, countY, countZ],
98
+ origin: { x: origin.x, y: origin.y, z: origin.z },
99
+ spacing,
100
+ min,
101
+ max,
102
+ mean,
103
+ sigma: Math.sqrt(Math.max(variance, 0)),
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Flat offset of a sample, the layout molstar expects.
109
+ * @param dimensions - `[nx, ny, nz]` of the grid.
110
+ * @param indexX - sample index along x.
111
+ * @param indexY - sample index along y.
112
+ * @param indexZ - sample index along z, the fastest-varying axis.
113
+ * @returns the index into {@link OrbitalGrid.data}.
114
+ */
115
+ export function gridIndex(
116
+ dimensions: [number, number, number],
117
+ indexX: number,
118
+ indexY: number,
119
+ indexZ: number,
120
+ ): number {
121
+ return (indexX * dimensions[1] + indexY) * dimensions[2] + indexZ;
122
+ }
123
+
124
+ function axisCount(edge: number, spacing: number): number {
125
+ return Math.max(2, Math.round(edge / spacing) + 1);
126
+ }