react-cheminfo 0.2.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.
- package/README.md +47 -6
- package/lib/core.d.ts +1 -0
- package/lib/core.d.ts.map +1 -1
- package/lib/core.js +1 -0
- package/lib/core.js.map +1 -1
- package/lib/ecosystem/core/sites.d.ts +1 -1
- package/lib/ecosystem/core/sites.d.ts.map +1 -1
- package/lib/ecosystem/core/sites.js +18 -0
- package/lib/ecosystem/core/sites.js.map +1 -1
- package/lib/ecosystem/ui/glyphs.d.ts +16 -0
- package/lib/ecosystem/ui/glyphs.d.ts.map +1 -0
- package/lib/ecosystem/ui/glyphs.js +41 -0
- package/lib/ecosystem/ui/glyphs.js.map +1 -0
- package/lib/ecosystem/ui/marks.d.ts.map +1 -1
- package/lib/ecosystem/ui/marks.js +2 -33
- package/lib/ecosystem/ui/marks.js.map +1 -1
- package/lib/orbital/core/atomicGrid.d.ts +58 -0
- package/lib/orbital/core/atomicGrid.d.ts.map +1 -0
- package/lib/orbital/core/atomicGrid.js +69 -0
- package/lib/orbital/core/atomicGrid.js.map +1 -0
- package/lib/orbital/core/atomicOrbitals.d.ts +97 -0
- package/lib/orbital/core/atomicOrbitals.d.ts.map +1 -0
- package/lib/orbital/core/atomicOrbitals.js +135 -0
- package/lib/orbital/core/atomicOrbitals.js.map +1 -0
- package/lib/orbital/core/constants.d.ts +33 -0
- package/lib/orbital/core/constants.d.ts.map +1 -0
- package/lib/orbital/core/constants.js +21 -0
- package/lib/orbital/core/constants.js.map +1 -0
- package/lib/orbital/core/electronConfiguration.d.ts +106 -0
- package/lib/orbital/core/electronConfiguration.d.ts.map +1 -0
- package/lib/orbital/core/electronConfiguration.js +207 -0
- package/lib/orbital/core/electronConfiguration.js.map +1 -0
- package/lib/orbital/core/grid.d.ts +60 -0
- package/lib/orbital/core/grid.d.ts.map +1 -0
- package/lib/orbital/core/grid.js +81 -0
- package/lib/orbital/core/grid.js.map +1 -0
- package/lib/orbital/core/hydrogenic.d.ts +99 -0
- package/lib/orbital/core/hydrogenic.d.ts.map +1 -0
- package/lib/orbital/core/hydrogenic.js +181 -0
- package/lib/orbital/core/hydrogenic.js.map +1 -0
- package/lib/orbital/core/index.d.ts +22 -0
- package/lib/orbital/core/index.d.ts.map +1 -0
- package/lib/orbital/core/index.js +12 -0
- package/lib/orbital/core/index.js.map +1 -0
- package/lib/orbital/core/numerics.d.ts +39 -0
- package/lib/orbital/core/numerics.d.ts.map +1 -0
- package/lib/orbital/core/numerics.js +75 -0
- package/lib/orbital/core/numerics.js.map +1 -0
- package/lib/orbital/core/occupancy.d.ts +44 -0
- package/lib/orbital/core/occupancy.d.ts.map +1 -0
- package/lib/orbital/core/occupancy.js +85 -0
- package/lib/orbital/core/occupancy.js.map +1 -0
- package/lib/orbital/core/palette.d.ts +36 -0
- package/lib/orbital/core/palette.d.ts.map +1 -0
- package/lib/orbital/core/palette.js +37 -0
- package/lib/orbital/core/palette.js.map +1 -0
- package/lib/orbital/core/realHarmonics.d.ts +57 -0
- package/lib/orbital/core/realHarmonics.d.ts.map +1 -0
- package/lib/orbital/core/realHarmonics.js +144 -0
- package/lib/orbital/core/realHarmonics.js.map +1 -0
- package/lib/orbital/core/sample.d.ts +52 -0
- package/lib/orbital/core/sample.d.ts.map +1 -0
- package/lib/orbital/core/sample.js +54 -0
- package/lib/orbital/core/sample.js.map +1 -0
- package/lib/orbital/core/screening.d.ts +46 -0
- package/lib/orbital/core/screening.d.ts.map +1 -0
- package/lib/orbital/core/screening.js +72 -0
- package/lib/orbital/core/screening.js.map +1 -0
- package/lib/orbital/ui/AtomicOrbitalCanvas.d.ts +57 -0
- package/lib/orbital/ui/AtomicOrbitalCanvas.d.ts.map +1 -0
- package/lib/orbital/ui/AtomicOrbitalCanvas.js +105 -0
- package/lib/orbital/ui/AtomicOrbitalCanvas.js.map +1 -0
- package/lib/orbital/ui/AtomicOrbitalViewer.d.ts +73 -0
- package/lib/orbital/ui/AtomicOrbitalViewer.d.ts.map +1 -0
- package/lib/orbital/ui/AtomicOrbitalViewer.js +49 -0
- package/lib/orbital/ui/AtomicOrbitalViewer.js.map +1 -0
- package/lib/orbital/ui/camera.d.ts +36 -0
- package/lib/orbital/ui/camera.d.ts.map +1 -0
- package/lib/orbital/ui/camera.js +98 -0
- package/lib/orbital/ui/camera.js.map +1 -0
- package/lib/orbital/ui/capability.d.ts +28 -0
- package/lib/orbital/ui/capability.d.ts.map +1 -0
- package/lib/orbital/ui/capability.js +77 -0
- package/lib/orbital/ui/capability.js.map +1 -0
- package/lib/orbital/ui/index.d.ts +17 -0
- package/lib/orbital/ui/index.d.ts.map +1 -0
- package/lib/orbital/ui/index.js +13 -0
- package/lib/orbital/ui/index.js.map +1 -0
- package/lib/orbital/ui/renderVolume.d.ts +57 -0
- package/lib/orbital/ui/renderVolume.d.ts.map +1 -0
- package/lib/orbital/ui/renderVolume.js +120 -0
- package/lib/orbital/ui/renderVolume.js.map +1 -0
- package/lib/orbital/ui/viewer.d.ts +86 -0
- package/lib/orbital/ui/viewer.d.ts.map +1 -0
- package/lib/orbital/ui/viewer.js +155 -0
- package/lib/orbital/ui/viewer.js.map +1 -0
- package/lib/orbital/ui/volumeField.d.ts +49 -0
- package/lib/orbital/ui/volumeField.d.ts.map +1 -0
- package/lib/orbital/ui/volumeField.js +108 -0
- package/lib/orbital/ui/volumeField.js.map +1 -0
- package/lib/orbital.d.ts +11 -0
- package/lib/orbital.d.ts.map +1 -0
- package/lib/orbital.js +11 -0
- package/lib/orbital.js.map +1 -0
- package/package.json +11 -3
- package/src/core.ts +1 -0
- package/src/ecosystem/core/sites.ts +21 -1
- package/src/ecosystem/ui/glyphs.tsx +246 -0
- package/src/ecosystem/ui/marks.tsx +4 -201
- package/src/orbital/core/atomicGrid.ts +115 -0
- package/src/orbital/core/atomicOrbitals.ts +212 -0
- package/src/orbital/core/constants.ts +36 -0
- package/src/orbital/core/electronConfiguration.ts +242 -0
- package/src/orbital/core/grid.ts +126 -0
- package/src/orbital/core/hydrogenic.ts +222 -0
- package/src/orbital/core/index.ts +72 -0
- package/src/orbital/core/numerics.ts +79 -0
- package/src/orbital/core/occupancy.ts +100 -0
- package/src/orbital/core/palette.ts +56 -0
- package/src/orbital/core/realHarmonics.ts +172 -0
- package/src/orbital/core/sample.ts +91 -0
- package/src/orbital/core/screening.ts +90 -0
- package/src/orbital/ui/AtomicOrbitalCanvas.tsx +181 -0
- package/src/orbital/ui/AtomicOrbitalViewer.tsx +150 -0
- package/src/orbital/ui/camera.ts +125 -0
- package/src/orbital/ui/capability.ts +102 -0
- package/src/orbital/ui/index.ts +17 -0
- package/src/orbital/ui/renderVolume.ts +190 -0
- package/src/orbital/ui/viewer.ts +186 -0
- package/src/orbital/ui/volumeField.ts +128 -0
- package/src/orbital.ts +11 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hydrogen-like atomic orbitals: the exact one-electron solutions, scaled to an
|
|
3
|
+
* element by an effective nuclear charge.
|
|
4
|
+
*
|
|
5
|
+
* `R(r) = N (2Zr/na₀)^ℓ e^(−Zr/na₀) L^(2ℓ+1)_(n−ℓ−1)(2Zr/na₀)`
|
|
6
|
+
*
|
|
7
|
+
* The associated Laguerre polynomial is the whole point. The Slater orbital a
|
|
8
|
+
* hybridisation model uses is `r^(n−1) e^(−ζr)`, which is **nodeless**: it draws
|
|
9
|
+
* a 2s exactly like a 1s, only fatter. That is fine for a hybrid lobe, where
|
|
10
|
+
* only the outer contour is ever seen, and wrong for a picture whose subject is
|
|
11
|
+
* what an orbital looks like — `n − ℓ − 1` radial nodes and ℓ angular ones is
|
|
12
|
+
* the first thing a student is asked to count.
|
|
13
|
+
*
|
|
14
|
+
* Amplitudes are in Å^(-3/2) and distances in ångström, like everything else
|
|
15
|
+
* here, so `∫|ψ|² dV` over a grid measured in ångström is 1.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { BOHR_IN_ANGSTROM, RYDBERG_ELECTRONVOLTS } from './constants.ts';
|
|
19
|
+
import {
|
|
20
|
+
assertQuantumNumbers,
|
|
21
|
+
bisectRoot,
|
|
22
|
+
factorial,
|
|
23
|
+
laguerre,
|
|
24
|
+
} from './numerics.ts';
|
|
25
|
+
|
|
26
|
+
/** Which hydrogen-like orbital, and how strongly it is bound. */
|
|
27
|
+
export interface HydrogenicParameters {
|
|
28
|
+
/** Principal quantum number, 1 and up. */
|
|
29
|
+
n: number;
|
|
30
|
+
/** Angular momentum quantum number, 0 to n − 1. */
|
|
31
|
+
l: number;
|
|
32
|
+
/** Effective nuclear charge, in units of the elementary charge. */
|
|
33
|
+
charge: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Radial nodes: spheres on which the wavefunction vanishes and changes sign.
|
|
38
|
+
* @param parameters - Which orbital.
|
|
39
|
+
* @returns `n − ℓ − 1`.
|
|
40
|
+
*/
|
|
41
|
+
export function radialNodeCount(parameters: HydrogenicParameters): number {
|
|
42
|
+
return parameters.n - parameters.l - 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Mean distance of the electron from the nucleus,
|
|
47
|
+
* `⟨r⟩ = a₀(3n² − ℓ(ℓ+1)) / 2Z`.
|
|
48
|
+
* @param parameters - Which orbital.
|
|
49
|
+
* @returns The expectation value in ångström.
|
|
50
|
+
*/
|
|
51
|
+
export function meanRadius(parameters: HydrogenicParameters): number {
|
|
52
|
+
const { n, l, charge } = parameters;
|
|
53
|
+
return (
|
|
54
|
+
(BOHR_IN_ANGSTROM * (3 * n * n - l * (l + 1))) /
|
|
55
|
+
(2 * Math.max(charge, 1e-6))
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Orbital energy in the hydrogen-like model, `E = −13.6 Z²/n²` eV.
|
|
61
|
+
* @param parameters - Which orbital.
|
|
62
|
+
* @returns The energy in electronvolts, always negative.
|
|
63
|
+
*/
|
|
64
|
+
export function orbitalEnergy(parameters: HydrogenicParameters): number {
|
|
65
|
+
const { n, charge } = parameters;
|
|
66
|
+
return (-RYDBERG_ELECTRONVOLTS * charge * charge) / (n * n);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Build the radial function `R(r)`, with every constant hoisted out so a grid
|
|
71
|
+
* sweep pays for the exponential and the polynomial only.
|
|
72
|
+
* @param parameters - Which orbital.
|
|
73
|
+
* @returns A function taking a distance in ångström and returning Å^(-3/2).
|
|
74
|
+
* @throws {Error} When the quantum numbers are not a real orbital, i.e. `n < 1` or
|
|
75
|
+
* `ℓ` outside `0 … n − 1`, or when the charge is not positive.
|
|
76
|
+
*/
|
|
77
|
+
export function createRadialFunction(
|
|
78
|
+
parameters: HydrogenicParameters,
|
|
79
|
+
): (distance: number) => number {
|
|
80
|
+
const { n, l, charge } = parameters;
|
|
81
|
+
assertQuantumNumbers(n, l);
|
|
82
|
+
if (!(charge > 0)) {
|
|
83
|
+
throw new RangeError(
|
|
84
|
+
`the effective charge must be positive, got ${charge}`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
const degree = n - l - 1;
|
|
88
|
+
const alpha = 2 * l + 1;
|
|
89
|
+
// ρ = 2Zr/(n a₀), with r in ångström, so a₀ appears once here and never again.
|
|
90
|
+
const rho = (2 * charge) / (n * BOHR_IN_ANGSTROM);
|
|
91
|
+
const normalisation = Math.sqrt(
|
|
92
|
+
rho * rho * rho * (factorial(degree) / (2 * n * factorial(n + l))),
|
|
93
|
+
);
|
|
94
|
+
return (distance) => {
|
|
95
|
+
const scaled = rho * distance;
|
|
96
|
+
return (
|
|
97
|
+
normalisation *
|
|
98
|
+
scaled ** l *
|
|
99
|
+
Math.exp(-scaled / 2) *
|
|
100
|
+
laguerre(degree, alpha, scaled)
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Evaluate the radial function once.
|
|
107
|
+
* @param parameters - Which orbital.
|
|
108
|
+
* @param distance - Distance from the nucleus, ångström.
|
|
109
|
+
* @returns The radial amplitude, Å^(-3/2).
|
|
110
|
+
*/
|
|
111
|
+
export function radialAmplitude(
|
|
112
|
+
parameters: HydrogenicParameters,
|
|
113
|
+
distance: number,
|
|
114
|
+
): number {
|
|
115
|
+
return createRadialFunction(parameters)(distance);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Where the radial nodes actually are.
|
|
120
|
+
*
|
|
121
|
+
* The `n − ℓ − 1` roots are bracketed by scanning outwards and then bisected,
|
|
122
|
+
* rather than solved for, because the Laguerre roots have no closed form past
|
|
123
|
+
* the quadratic and a bisection on a function we already evaluate cannot drift
|
|
124
|
+
* out of step with what is drawn.
|
|
125
|
+
* @param parameters - Which orbital.
|
|
126
|
+
* @param limit - Distance to search out to, ångström. Defaults to a box that
|
|
127
|
+
* comfortably holds the outermost node.
|
|
128
|
+
* @returns The node radii in ångström, ascending; empty for a nodeless orbital.
|
|
129
|
+
*/
|
|
130
|
+
export function radialNodeRadii(
|
|
131
|
+
parameters: HydrogenicParameters,
|
|
132
|
+
limit = meanRadius(parameters) * 3,
|
|
133
|
+
): number[] {
|
|
134
|
+
const expected = radialNodeCount(parameters);
|
|
135
|
+
if (expected === 0) return [];
|
|
136
|
+
const radial = createRadialFunction(parameters);
|
|
137
|
+
const radii: number[] = [];
|
|
138
|
+
const steps = 4000;
|
|
139
|
+
let previousDistance = 1e-6;
|
|
140
|
+
let previous = radial(previousDistance);
|
|
141
|
+
for (let step = 1; step <= steps; step++) {
|
|
142
|
+
const distance = (limit * step) / steps;
|
|
143
|
+
const value = radial(distance);
|
|
144
|
+
if (previous === 0 || previous < 0 !== value < 0) {
|
|
145
|
+
radii.push(bisectRoot(radial, previousDistance, distance));
|
|
146
|
+
if (radii.length === expected) break;
|
|
147
|
+
}
|
|
148
|
+
previousDistance = distance;
|
|
149
|
+
previous = value;
|
|
150
|
+
}
|
|
151
|
+
return radii;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Smallest sphere holding a given share of the electron.
|
|
156
|
+
*
|
|
157
|
+
* Used to size the sampling box: too small clips the orbital at a face, too
|
|
158
|
+
* large wastes the grid on empty space, and both are visible on screen.
|
|
159
|
+
* @param parameters - Which orbital.
|
|
160
|
+
* @param fraction - Share of `∫ r²R² dr` to enclose, between 0 and 1.
|
|
161
|
+
* @returns The radius in ångström.
|
|
162
|
+
* @throws {Error} When `fraction` is outside `(0, 1)`.
|
|
163
|
+
*/
|
|
164
|
+
export function enclosingRadius(
|
|
165
|
+
parameters: HydrogenicParameters,
|
|
166
|
+
fraction: number,
|
|
167
|
+
): number {
|
|
168
|
+
if (!(fraction > 0) || fraction >= 1) {
|
|
169
|
+
throw new RangeError(`the enclosed fraction must be in (0, 1)`);
|
|
170
|
+
}
|
|
171
|
+
const radial = createRadialFunction(parameters);
|
|
172
|
+
// The tail decays as e^(−2Zr/n a₀); ten mean radii is far past every node.
|
|
173
|
+
const limit = meanRadius(parameters) * 10;
|
|
174
|
+
const steps = 4000;
|
|
175
|
+
const step = limit / steps;
|
|
176
|
+
const weights = new Float64Array(steps + 1);
|
|
177
|
+
let total = 0;
|
|
178
|
+
for (let index = 0; index <= steps; index++) {
|
|
179
|
+
const distance = index * step;
|
|
180
|
+
const value = radial(distance);
|
|
181
|
+
const weight = value * value * distance * distance;
|
|
182
|
+
weights[index] = weight;
|
|
183
|
+
total += weight;
|
|
184
|
+
}
|
|
185
|
+
if (total === 0) return limit;
|
|
186
|
+
let running = 0;
|
|
187
|
+
const target = total * fraction;
|
|
188
|
+
for (let index = 0; index <= steps; index++) {
|
|
189
|
+
running += weights[index] as number;
|
|
190
|
+
if (running >= target) return index * step;
|
|
191
|
+
}
|
|
192
|
+
return limit;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Sample the radial distribution `P(r) = r²R(r)²` — the curve whose zeroes are
|
|
197
|
+
* the radial nodes and whose area is the electron.
|
|
198
|
+
* @param parameters - Which orbital.
|
|
199
|
+
* @param limit - Distance to sample out to, ångström.
|
|
200
|
+
* @param count - Number of samples, at least 2.
|
|
201
|
+
* @returns Distances and the amplitude `R`, plus `P`, at each of them.
|
|
202
|
+
* @throws {Error} When `count` is below 2.
|
|
203
|
+
*/
|
|
204
|
+
export function radialProfile(
|
|
205
|
+
parameters: HydrogenicParameters,
|
|
206
|
+
limit: number,
|
|
207
|
+
count: number,
|
|
208
|
+
): { distances: Float64Array; amplitude: Float64Array; density: Float64Array } {
|
|
209
|
+
if (count < 2) throw new RangeError('a radial profile needs 2 samples');
|
|
210
|
+
const radial = createRadialFunction(parameters);
|
|
211
|
+
const distances = new Float64Array(count);
|
|
212
|
+
const amplitude = new Float64Array(count);
|
|
213
|
+
const density = new Float64Array(count);
|
|
214
|
+
for (let index = 0; index < count; index++) {
|
|
215
|
+
const distance = (limit * index) / (count - 1);
|
|
216
|
+
const value = radial(distance);
|
|
217
|
+
distances[index] = distance;
|
|
218
|
+
amplitude[index] = value;
|
|
219
|
+
density[index] = value * value * distance * distance;
|
|
220
|
+
}
|
|
221
|
+
return { distances, amplitude, density };
|
|
222
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export type { AtomicGridOptions } from './atomicGrid.ts';
|
|
2
|
+
export {
|
|
3
|
+
atomicGridBox,
|
|
4
|
+
createAtomicOrbitalEvaluator,
|
|
5
|
+
sampleAtomicOrbital,
|
|
6
|
+
} from './atomicGrid.ts';
|
|
7
|
+
export type { AtomicOrbital, AtomicOrbitalOptions } from './atomicOrbitals.ts';
|
|
8
|
+
export {
|
|
9
|
+
atomicOrbitalsOf,
|
|
10
|
+
defaultOrbitalId,
|
|
11
|
+
findAtomicOrbital,
|
|
12
|
+
hydrogenicParametersOf,
|
|
13
|
+
orbitalId,
|
|
14
|
+
} from './atomicOrbitals.ts';
|
|
15
|
+
export type { Vec3 } from './constants.ts';
|
|
16
|
+
export {
|
|
17
|
+
BOHR_IN_ANGSTROM,
|
|
18
|
+
ENCLOSED_WEIGHT,
|
|
19
|
+
RYDBERG_ELECTRONVOLTS,
|
|
20
|
+
} from './constants.ts';
|
|
21
|
+
export type { Subshell, SubshellOccupancy } from './electronConfiguration.ts';
|
|
22
|
+
export {
|
|
23
|
+
ELEMENT_ANOMALIES,
|
|
24
|
+
HIGHEST_ATOMIC_NUMBER,
|
|
25
|
+
MADELUNG_ORDER,
|
|
26
|
+
NOBLE_GASES,
|
|
27
|
+
assertAtomicNumber,
|
|
28
|
+
aufbauConfigurationOf,
|
|
29
|
+
configurationOf,
|
|
30
|
+
coreAtomicNumber,
|
|
31
|
+
formatConfiguration,
|
|
32
|
+
formatOccupancy,
|
|
33
|
+
isAnomalous,
|
|
34
|
+
subshellCapacity,
|
|
35
|
+
subshellLabel,
|
|
36
|
+
superscript,
|
|
37
|
+
} from './electronConfiguration.ts';
|
|
38
|
+
export type { GridBox, OrbitalEvaluator, OrbitalGrid } from './grid.ts';
|
|
39
|
+
export { evaluateGrid, gridIndex } from './grid.ts';
|
|
40
|
+
export type { HydrogenicParameters } from './hydrogenic.ts';
|
|
41
|
+
export {
|
|
42
|
+
createRadialFunction,
|
|
43
|
+
enclosingRadius,
|
|
44
|
+
meanRadius,
|
|
45
|
+
orbitalEnergy,
|
|
46
|
+
radialAmplitude,
|
|
47
|
+
radialNodeCount,
|
|
48
|
+
radialNodeRadii,
|
|
49
|
+
radialProfile,
|
|
50
|
+
} from './hydrogenic.ts';
|
|
51
|
+
export {
|
|
52
|
+
hundDistribution,
|
|
53
|
+
outermostShell,
|
|
54
|
+
withoutOutermostElectron,
|
|
55
|
+
} from './occupancy.ts';
|
|
56
|
+
export type { PhasePalette, PhasePaletteId } from './palette.ts';
|
|
57
|
+
export { DEFAULT_PHASE_PALETTE_ID, PHASE_PALETTES } from './palette.ts';
|
|
58
|
+
export type { RealHarmonic } from './realHarmonics.ts';
|
|
59
|
+
export {
|
|
60
|
+
REAL_HARMONICS,
|
|
61
|
+
SUBSHELL_LETTERS,
|
|
62
|
+
harmonicsOf,
|
|
63
|
+
subshellLetter,
|
|
64
|
+
} from './realHarmonics.ts';
|
|
65
|
+
export type {
|
|
66
|
+
AtomicSampleRequest,
|
|
67
|
+
AtomicSampleResult,
|
|
68
|
+
AtomicSampler,
|
|
69
|
+
} from './sample.ts';
|
|
70
|
+
export { runAtomicSample, sampleInProcess } from './sample.ts';
|
|
71
|
+
export type { Screening } from './screening.ts';
|
|
72
|
+
export { groupRank, slaterScreening } from './screening.ts';
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The numerical machinery the radial functions are built on.
|
|
3
|
+
*
|
|
4
|
+
* Kept apart from `hydrogenic.ts` so that file reads as the physics it is —
|
|
5
|
+
* `R(r)`, `⟨r⟩`, the nodes, the enclosing sphere — with none of it buried under
|
|
6
|
+
* a polynomial recurrence and a bisection.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The generalised Laguerre polynomial `L^α_k(x)`, by its three-term recurrence.
|
|
11
|
+
*
|
|
12
|
+
* The closed form overflows well before `n = 7`; `k` is at most 6 here.
|
|
13
|
+
* @param degree - Degree `k` of the polynomial.
|
|
14
|
+
* @param alpha - Order `α` of the polynomial.
|
|
15
|
+
* @param x - Where to evaluate it.
|
|
16
|
+
* @returns The value of the polynomial.
|
|
17
|
+
*/
|
|
18
|
+
export function laguerre(degree: number, alpha: number, x: number): number {
|
|
19
|
+
if (degree === 0) return 1;
|
|
20
|
+
let previous = 1;
|
|
21
|
+
let current = 1 + alpha - x;
|
|
22
|
+
for (let index = 1; index < degree; index++) {
|
|
23
|
+
const next =
|
|
24
|
+
((2 * index + 1 + alpha - x) * current - (index + alpha) * previous) /
|
|
25
|
+
(index + 1);
|
|
26
|
+
previous = current;
|
|
27
|
+
current = next;
|
|
28
|
+
}
|
|
29
|
+
return current;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The root of a function inside a bracket it already changes sign over.
|
|
34
|
+
* @param radial - The function to find a zero of.
|
|
35
|
+
* @param low - Lower end of the bracket.
|
|
36
|
+
* @param high - Upper end.
|
|
37
|
+
* @returns The root, to about a picometre.
|
|
38
|
+
*/
|
|
39
|
+
export function bisectRoot(
|
|
40
|
+
radial: (distance: number) => number,
|
|
41
|
+
low: number,
|
|
42
|
+
high: number,
|
|
43
|
+
): number {
|
|
44
|
+
let left = low;
|
|
45
|
+
let right = high;
|
|
46
|
+
const leftSign = radial(left) < 0;
|
|
47
|
+
for (let step = 0; step < 60 && right - left > 1e-9; step++) {
|
|
48
|
+
const middle = (left + right) / 2;
|
|
49
|
+
if (radial(middle) < 0 === leftSign) left = middle;
|
|
50
|
+
else right = middle;
|
|
51
|
+
}
|
|
52
|
+
return (left + right) / 2;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Reject quantum numbers that are not a real orbital.
|
|
57
|
+
* @param n - Principal quantum number.
|
|
58
|
+
* @param l - Angular momentum quantum number.
|
|
59
|
+
* @throws {RangeError} When `n < 1`, or ℓ is outside `0 … n − 1`.
|
|
60
|
+
*/
|
|
61
|
+
export function assertQuantumNumbers(n: number, l: number): void {
|
|
62
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
63
|
+
throw new RangeError(`the principal quantum number must be 1 or more`);
|
|
64
|
+
}
|
|
65
|
+
if (!Number.isInteger(l) || l < 0 || l >= n) {
|
|
66
|
+
throw new RangeError(`ℓ must be between 0 and ${n - 1} for n = ${n}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The factorial of a small non-negative integer.
|
|
72
|
+
* @param value - The integer.
|
|
73
|
+
* @returns Its factorial.
|
|
74
|
+
*/
|
|
75
|
+
export function factorial(value: number): number {
|
|
76
|
+
let result = 1;
|
|
77
|
+
for (let index = 2; index <= value; index++) result *= index;
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How the electrons of one atom are laid out over its orbitals, and the two
|
|
3
|
+
* adjustments a *virtual* orbital needs.
|
|
4
|
+
*
|
|
5
|
+
* Split out of `atomicOrbitals.ts` so that module stays what it says it is —
|
|
6
|
+
* the join of a configuration, a screening, a radial function and a harmonic —
|
|
7
|
+
* with none of the bookkeeping in the way.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { SubshellOccupancy } from './electronConfiguration.ts';
|
|
11
|
+
import { groupRank } from './screening.ts';
|
|
12
|
+
|
|
13
|
+
/** No known element occupies a shell above n = 7. */
|
|
14
|
+
const HIGHEST_SHELL = 7;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Slater's rules can hand a badly-screened orbital a charge at or below zero —
|
|
18
|
+
* a 7p on hydrogen, say — which has no bound hydrogen-like solution. Clamping
|
|
19
|
+
* to a small positive charge keeps such an orbital enormous and diffuse, which
|
|
20
|
+
* is the honest picture, rather than throwing.
|
|
21
|
+
*/
|
|
22
|
+
export const MINIMUM_CHARGE = 0.3;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Spread electrons over the orbitals of a subshell by Hund's rule.
|
|
26
|
+
* @param electrons - Electrons in the subshell.
|
|
27
|
+
* @param orbitalCount - `2ℓ + 1`.
|
|
28
|
+
* @returns One count per orbital: singles first, then pairs.
|
|
29
|
+
*/
|
|
30
|
+
export function hundDistribution(
|
|
31
|
+
electrons: number,
|
|
32
|
+
orbitalCount: number,
|
|
33
|
+
): number[] {
|
|
34
|
+
const spread = new Array<number>(orbitalCount).fill(0);
|
|
35
|
+
const capacity = orbitalCount * 2;
|
|
36
|
+
let left = Math.min(electrons, capacity);
|
|
37
|
+
for (let pass = 0; pass < 2 && left > 0; pass++) {
|
|
38
|
+
for (let index = 0; index < orbitalCount && left > 0; index++) {
|
|
39
|
+
spread[index] = (spread[index] as number) + 1;
|
|
40
|
+
left -= 1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return spread;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* How far up the list goes when the caller does not say.
|
|
48
|
+
* @param valenceShell - Outermost occupied shell.
|
|
49
|
+
* @returns One shell past it, never below 4 and never above 7.
|
|
50
|
+
*/
|
|
51
|
+
export function defaultMaximumShell(valenceShell: number): number {
|
|
52
|
+
return Math.min(HIGHEST_SHELL, Math.max(4, valenceShell + 1));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The configuration with one electron taken out of the outermost occupied
|
|
57
|
+
* subshell — the atom as it is while one of its electrons sits in a virtual
|
|
58
|
+
* orbital.
|
|
59
|
+
* @param configuration - The ground-state configuration to reduce.
|
|
60
|
+
* @returns The reduced configuration, empty subshells dropped.
|
|
61
|
+
*/
|
|
62
|
+
export function withoutOutermostElectron(
|
|
63
|
+
configuration: readonly SubshellOccupancy[],
|
|
64
|
+
): SubshellOccupancy[] {
|
|
65
|
+
let outermost = -1;
|
|
66
|
+
for (let index = 0; index < configuration.length; index++) {
|
|
67
|
+
const entry = configuration[index] as SubshellOccupancy;
|
|
68
|
+
const best = configuration[outermost];
|
|
69
|
+
if (
|
|
70
|
+
outermost === -1 ||
|
|
71
|
+
best === undefined ||
|
|
72
|
+
groupRank(entry) > groupRank(best)
|
|
73
|
+
) {
|
|
74
|
+
outermost = index;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const reduced: SubshellOccupancy[] = [];
|
|
78
|
+
for (let index = 0; index < configuration.length; index++) {
|
|
79
|
+
const entry = configuration[index] as SubshellOccupancy;
|
|
80
|
+
const electrons =
|
|
81
|
+
index === outermost ? entry.electrons - 1 : entry.electrons;
|
|
82
|
+
if (electrons > 0) reduced.push({ ...entry, electrons });
|
|
83
|
+
}
|
|
84
|
+
return reduced;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The outermost shell an atom actually occupies.
|
|
89
|
+
* @param configuration - Its configuration.
|
|
90
|
+
* @returns The largest `n` holding an electron.
|
|
91
|
+
*/
|
|
92
|
+
export function outermostShell(
|
|
93
|
+
configuration: readonly SubshellOccupancy[],
|
|
94
|
+
): number {
|
|
95
|
+
let outermost = 1;
|
|
96
|
+
for (const entry of configuration) {
|
|
97
|
+
if (entry.electrons > 0 && entry.n > outermost) outermost = entry.n;
|
|
98
|
+
}
|
|
99
|
+
return outermost;
|
|
100
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Colours for orbital phases and nodal surfaces.
|
|
3
|
+
*
|
|
4
|
+
* Plain data: the renderer picks a palette, this module says what the hex
|
|
5
|
+
* values are. Two phase palettes ship, and both are first-class — around 8% of
|
|
6
|
+
* male students have a red/green or red/blue-at-low-luminance deficiency, so
|
|
7
|
+
* the blue/yellow pair is a supported choice, not a fallback.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Which pair of phase colours the viewer uses. */
|
|
11
|
+
export type PhasePaletteId = 'textbook' | 'colourBlindSafe';
|
|
12
|
+
|
|
13
|
+
/** The colours one phase palette provides. */
|
|
14
|
+
export interface PhasePalette {
|
|
15
|
+
id: PhasePaletteId;
|
|
16
|
+
/** Name shown in the palette picker. */
|
|
17
|
+
label: string;
|
|
18
|
+
/** Lobe where the wavefunction is positive. */
|
|
19
|
+
positive: string;
|
|
20
|
+
/** Lobe where the wavefunction is negative. */
|
|
21
|
+
negative: string;
|
|
22
|
+
/** Lobe drawn with the `phase` display flag off. */
|
|
23
|
+
neutral: string;
|
|
24
|
+
/** Translucent disc marking a nodal plane. */
|
|
25
|
+
node: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The two phase palettes.
|
|
30
|
+
*
|
|
31
|
+
* `textbook` is the blue/red of printed MO diagrams and of Jmol's signed
|
|
32
|
+
* isosurfaces; `colourBlindSafe` swaps the negative lobe for the amber of the
|
|
33
|
+
* Okabe-Ito qualitative palette, which stays separable under protanopia,
|
|
34
|
+
* deuteranopia and tritanopia.
|
|
35
|
+
*/
|
|
36
|
+
export const PHASE_PALETTES: Record<PhasePaletteId, PhasePalette> = {
|
|
37
|
+
textbook: {
|
|
38
|
+
id: 'textbook',
|
|
39
|
+
label: 'Blue / red (textbook)',
|
|
40
|
+
positive: '#2563eb',
|
|
41
|
+
negative: '#dc2626',
|
|
42
|
+
neutral: '#9ca3af',
|
|
43
|
+
node: '#64748b',
|
|
44
|
+
},
|
|
45
|
+
colourBlindSafe: {
|
|
46
|
+
id: 'colourBlindSafe',
|
|
47
|
+
label: 'Blue / yellow (colour-blind safe)',
|
|
48
|
+
positive: '#0072b2',
|
|
49
|
+
negative: '#e69f00',
|
|
50
|
+
neutral: '#9ca3af',
|
|
51
|
+
node: '#56606b',
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** Palette used until the student picks another one. */
|
|
56
|
+
export const DEFAULT_PHASE_PALETTE_ID: PhasePaletteId = 'textbook';
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The real spherical harmonics, ℓ = 0 to 3, written as cartesian ratios.
|
|
3
|
+
*
|
|
4
|
+
* These are the angular halves of the orbitals a chemist draws: the real
|
|
5
|
+
* combinations `p_x, p_y, p_z` rather than the complex `m = −1, 0, +1`, because
|
|
6
|
+
* only the real ones have the lobes along axes that a bond can point at.
|
|
7
|
+
*
|
|
8
|
+
* Each is normalised so that `∫|Y|² dΩ = 1` over the unit sphere, and each is
|
|
9
|
+
* given `(x, y, z)` in ångström together with their length, so sampling a grid
|
|
10
|
+
* never calls `acos` or `atan2`. The number of angular nodes is exactly ℓ, and
|
|
11
|
+
* this file is where that fact becomes visible: `s` has none, every `p` has one
|
|
12
|
+
* plane, every `d` two surfaces, every `f` three.
|
|
13
|
+
*
|
|
14
|
+
* `key` is ascii and appears in a URL — `#/elements/Fe/3dx2-y2`; `label` is the
|
|
15
|
+
* typeset subscript a student reads.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Subshell letter for an angular momentum, `s p d f`. */
|
|
19
|
+
export const SUBSHELL_LETTERS = ['s', 'p', 'd', 'f'] as const;
|
|
20
|
+
|
|
21
|
+
/** One real spherical harmonic. */
|
|
22
|
+
export interface RealHarmonic {
|
|
23
|
+
/** Angular momentum quantum number, also the count of angular nodes. */
|
|
24
|
+
l: number;
|
|
25
|
+
/** Ascii subscript, url-safe: `x`, `z2`, `x2-y2`, `y_3x2-y2`. */
|
|
26
|
+
key: string;
|
|
27
|
+
/** Typeset subscript, e.g. `x²−y²`. Empty for `s`. */
|
|
28
|
+
label: string;
|
|
29
|
+
/**
|
|
30
|
+
* Value of the harmonic at a point.
|
|
31
|
+
* @param x - Offset from the nucleus along x, ångström.
|
|
32
|
+
* @param y - Offset from the nucleus along y, ångström.
|
|
33
|
+
* @param z - Offset from the nucleus along z, ångström.
|
|
34
|
+
* @param radius - Length of `(x, y, z)`, passed in because the caller has
|
|
35
|
+
* already computed it for the radial part.
|
|
36
|
+
* @returns The dimensionless amplitude; 0 at the nucleus for ℓ > 0.
|
|
37
|
+
*/
|
|
38
|
+
evaluate: (x: number, y: number, z: number, radius: number) => number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const FOUR_PI = 4 * Math.PI;
|
|
42
|
+
|
|
43
|
+
const S = Math.sqrt(1 / FOUR_PI);
|
|
44
|
+
const P = Math.sqrt(3 / FOUR_PI);
|
|
45
|
+
const D_Z2 = Math.sqrt(5 / (16 * Math.PI));
|
|
46
|
+
const D_PLANAR = Math.sqrt(15 / FOUR_PI);
|
|
47
|
+
const D_X2Y2 = Math.sqrt(15 / (16 * Math.PI));
|
|
48
|
+
const F_Z3 = Math.sqrt(7 / (16 * Math.PI));
|
|
49
|
+
const F_XZ2 = Math.sqrt(21 / (32 * Math.PI));
|
|
50
|
+
const F_ZX2Y2 = Math.sqrt(105 / (16 * Math.PI));
|
|
51
|
+
const F_XYZ = Math.sqrt(105 / FOUR_PI);
|
|
52
|
+
const F_CUBIC = Math.sqrt(35 / (32 * Math.PI));
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The harmonics of one shell, indexed by ℓ: `REAL_HARMONICS[2]` is the five d
|
|
56
|
+
* functions. The order inside a shell is the one the orbital list shows.
|
|
57
|
+
*/
|
|
58
|
+
export const REAL_HARMONICS: RealHarmonic[][] = [
|
|
59
|
+
[{ l: 0, key: '', label: '', evaluate: () => S }],
|
|
60
|
+
[
|
|
61
|
+
{ l: 1, key: 'x', label: 'x', evaluate: (x, y, z, r) => (P * x) / r },
|
|
62
|
+
{ l: 1, key: 'y', label: 'y', evaluate: (x, y, z, r) => (P * y) / r },
|
|
63
|
+
{ l: 1, key: 'z', label: 'z', evaluate: (x, y, z, r) => (P * z) / r },
|
|
64
|
+
],
|
|
65
|
+
[
|
|
66
|
+
{
|
|
67
|
+
l: 2,
|
|
68
|
+
key: 'z2',
|
|
69
|
+
label: 'z²',
|
|
70
|
+
evaluate: (x, y, z, r) => (D_Z2 * (3 * z * z - r * r)) / (r * r),
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
l: 2,
|
|
74
|
+
key: 'xz',
|
|
75
|
+
label: 'xz',
|
|
76
|
+
evaluate: (x, y, z, r) => (D_PLANAR * x * z) / (r * r),
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
l: 2,
|
|
80
|
+
key: 'yz',
|
|
81
|
+
label: 'yz',
|
|
82
|
+
evaluate: (x, y, z, r) => (D_PLANAR * y * z) / (r * r),
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
l: 2,
|
|
86
|
+
key: 'xy',
|
|
87
|
+
label: 'xy',
|
|
88
|
+
evaluate: (x, y, z, r) => (D_PLANAR * x * y) / (r * r),
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
l: 2,
|
|
92
|
+
key: 'x2-y2',
|
|
93
|
+
label: 'x²−y²',
|
|
94
|
+
evaluate: (x, y, z, r) => (D_X2Y2 * (x * x - y * y)) / (r * r),
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
[
|
|
98
|
+
{
|
|
99
|
+
l: 3,
|
|
100
|
+
key: 'z3',
|
|
101
|
+
label: 'z³',
|
|
102
|
+
evaluate: (x, y, z, r) =>
|
|
103
|
+
(F_Z3 * z * (5 * z * z - 3 * r * r)) / (r * r * r),
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
l: 3,
|
|
107
|
+
key: 'xz2',
|
|
108
|
+
label: 'xz²',
|
|
109
|
+
evaluate: (x, y, z, r) => (F_XZ2 * x * (5 * z * z - r * r)) / (r * r * r),
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
l: 3,
|
|
113
|
+
key: 'yz2',
|
|
114
|
+
label: 'yz²',
|
|
115
|
+
evaluate: (x, y, z, r) => (F_XZ2 * y * (5 * z * z - r * r)) / (r * r * r),
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
l: 3,
|
|
119
|
+
key: 'xyz',
|
|
120
|
+
label: 'xyz',
|
|
121
|
+
evaluate: (x, y, z, r) => (F_XYZ * x * y * z) / (r * r * r),
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
l: 3,
|
|
125
|
+
key: 'z_x2-y2',
|
|
126
|
+
label: 'z(x²−y²)',
|
|
127
|
+
evaluate: (x, y, z, r) => (F_ZX2Y2 * z * (x * x - y * y)) / (r * r * r),
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
l: 3,
|
|
131
|
+
key: 'x_x2-3y2',
|
|
132
|
+
label: 'x(x²−3y²)',
|
|
133
|
+
evaluate: (x, y, z, r) =>
|
|
134
|
+
(F_CUBIC * x * (x * x - 3 * y * y)) / (r * r * r),
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
l: 3,
|
|
138
|
+
key: 'y_3x2-y2',
|
|
139
|
+
label: 'y(3x²−y²)',
|
|
140
|
+
evaluate: (x, y, z, r) =>
|
|
141
|
+
(F_CUBIC * y * (3 * x * x - y * y)) / (r * r * r),
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The harmonics of one subshell.
|
|
148
|
+
* @param l - Angular momentum quantum number, 0 to 3.
|
|
149
|
+
* @returns Its `2ℓ + 1` real harmonics, in list order.
|
|
150
|
+
* @throws {Error} When ℓ is outside the s, p, d and f shells this site draws.
|
|
151
|
+
*/
|
|
152
|
+
export function harmonicsOf(l: number): RealHarmonic[] {
|
|
153
|
+
const shell = REAL_HARMONICS[l];
|
|
154
|
+
if (shell === undefined) {
|
|
155
|
+
throw new RangeError(`no real harmonics for angular momentum ${l}`);
|
|
156
|
+
}
|
|
157
|
+
return shell;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The letter naming a subshell.
|
|
162
|
+
* @param l - Angular momentum quantum number, 0 to 3.
|
|
163
|
+
* @returns `s`, `p`, `d` or `f`.
|
|
164
|
+
* @throws {Error} When ℓ is outside them.
|
|
165
|
+
*/
|
|
166
|
+
export function subshellLetter(l: number): string {
|
|
167
|
+
const letter = SUBSHELL_LETTERS[l];
|
|
168
|
+
if (letter === undefined) {
|
|
169
|
+
throw new RangeError(`no subshell letter for angular momentum ${l}`);
|
|
170
|
+
}
|
|
171
|
+
return letter;
|
|
172
|
+
}
|