@driftengine/chemistry 3.61.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/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +194 -0
- package/dist/conservation.d.ts +9 -0
- package/dist/conservation.js +38 -0
- package/dist/element/elements.d.ts +31 -0
- package/dist/element/elements.js +73 -0
- package/dist/element/matrix.d.ts +27 -0
- package/dist/element/matrix.js +80 -0
- package/dist/field/ambient.d.ts +49 -0
- package/dist/field/ambient.js +82 -0
- package/dist/field/atmosphere.d.ts +308 -0
- package/dist/field/atmosphere.js +1033 -0
- package/dist/ignition/criteria.d.ts +97 -0
- package/dist/ignition/criteria.js +135 -0
- package/dist/ignition/events.d.ts +56 -0
- package/dist/ignition/events.js +90 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.js +66 -0
- package/dist/library/biological.d.ts +6 -0
- package/dist/library/biological.js +229 -0
- package/dist/library/common.d.ts +26 -0
- package/dist/library/common.js +208 -0
- package/dist/library/food.d.ts +9 -0
- package/dist/library/food.js +272 -0
- package/dist/library/fuel.d.ts +19 -0
- package/dist/library/fuel.js +290 -0
- package/dist/library/library.d.ts +45 -0
- package/dist/library/library.js +29 -0
- package/dist/library/metal.d.ts +6 -0
- package/dist/library/metal.js +181 -0
- package/dist/library/mineral.d.ts +4 -0
- package/dist/library/mineral.js +177 -0
- package/dist/library/organic.d.ts +12 -0
- package/dist/library/organic.js +489 -0
- package/dist/library/polymer.d.ts +4 -0
- package/dist/library/polymer.js +237 -0
- package/dist/parcel/conduction.d.ts +20 -0
- package/dist/parcel/conduction.js +160 -0
- package/dist/parcel/shells.d.ts +49 -0
- package/dist/parcel/shells.js +73 -0
- package/dist/parcel/store.d.ts +395 -0
- package/dist/parcel/store.js +1152 -0
- package/dist/parcel/tier.d.ts +48 -0
- package/dist/parcel/tier.js +51 -0
- package/dist/present/audio.d.ts +41 -0
- package/dist/present/audio.js +50 -0
- package/dist/present/blackbody.d.ts +55 -0
- package/dist/present/blackbody.js +112 -0
- package/dist/present/flame.d.ts +34 -0
- package/dist/present/flame.js +103 -0
- package/dist/present/index.d.ts +25 -0
- package/dist/present/index.js +22 -0
- package/dist/present/install.d.ts +87 -0
- package/dist/present/install.js +157 -0
- package/dist/present/smoke.d.ts +63 -0
- package/dist/present/smoke.js +85 -0
- package/dist/present/surface.d.ts +40 -0
- package/dist/present/surface.js +76 -0
- package/dist/reaction/localise.d.ts +16 -0
- package/dist/reaction/localise.js +95 -0
- package/dist/reaction/rates.d.ts +59 -0
- package/dist/reaction/rates.js +101 -0
- package/dist/reaction/reaction.d.ts +178 -0
- package/dist/reaction/reaction.js +104 -0
- package/dist/reaction/registry.d.ts +47 -0
- package/dist/reaction/registry.js +215 -0
- package/dist/reaction/solver.d.ts +51 -0
- package/dist/reaction/solver.js +277 -0
- package/dist/species/registry.d.ts +26 -0
- package/dist/species/registry.js +97 -0
- package/dist/species/species.d.ts +48 -0
- package/dist/species/species.js +20 -0
- package/dist/species/standard.d.ts +30 -0
- package/dist/species/standard.js +436 -0
- package/dist/species/thermal.d.ts +18 -0
- package/dist/species/thermal.js +72 -0
- package/dist/substance/boiling.d.ts +29 -0
- package/dist/substance/boiling.js +54 -0
- package/dist/substance/composition.d.ts +34 -0
- package/dist/substance/composition.js +53 -0
- package/dist/substance/registry.d.ts +86 -0
- package/dist/substance/registry.js +187 -0
- package/dist/substance/substance.d.ts +94 -0
- package/dist/substance/substance.js +48 -0
- package/dist/testing/drift.d.ts +15 -0
- package/dist/testing/drift.js +29 -0
- package/dist/transport/contacts.d.ts +22 -0
- package/dist/transport/contacts.js +50 -0
- package/dist/transport/radiation.d.ts +101 -0
- package/dist/transport/radiation.js +199 -0
- package/dist/transport/world.d.ts +92 -0
- package/dist/transport/world.js +381 -0
- package/dist/world/fingerprint.d.ts +31 -0
- package/dist/world/fingerprint.js +70 -0
- package/package.json +100 -0
- package/src/conservation.ts +44 -0
- package/src/element/elements.ts +80 -0
- package/src/element/matrix.ts +89 -0
- package/src/field/ambient.ts +111 -0
- package/src/field/atmosphere.ts +1159 -0
- package/src/ignition/criteria.ts +177 -0
- package/src/ignition/events.ts +100 -0
- package/src/index.ts +213 -0
- package/src/library/biological.ts +252 -0
- package/src/library/common.ts +224 -0
- package/src/library/food.ts +302 -0
- package/src/library/fuel.ts +299 -0
- package/src/library/library.ts +62 -0
- package/src/library/metal.ts +186 -0
- package/src/library/mineral.ts +194 -0
- package/src/library/organic.ts +545 -0
- package/src/library/polymer.ts +243 -0
- package/src/parcel/conduction.ts +188 -0
- package/src/parcel/shells.ts +100 -0
- package/src/parcel/store.ts +1270 -0
- package/src/parcel/tier.ts +60 -0
- package/src/present/audio.ts +54 -0
- package/src/present/blackbody.ts +118 -0
- package/src/present/flame.ts +111 -0
- package/src/present/index.ts +25 -0
- package/src/present/install.ts +209 -0
- package/src/present/smoke.ts +139 -0
- package/src/present/surface.ts +107 -0
- package/src/reaction/localise.ts +126 -0
- package/src/reaction/rates.ts +153 -0
- package/src/reaction/reaction.ts +235 -0
- package/src/reaction/registry.ts +257 -0
- package/src/reaction/solver.ts +326 -0
- package/src/species/registry.ts +117 -0
- package/src/species/species.ts +53 -0
- package/src/species/standard.ts +449 -0
- package/src/species/thermal.ts +74 -0
- package/src/substance/boiling.ts +64 -0
- package/src/substance/composition.ts +81 -0
- package/src/substance/registry.ts +223 -0
- package/src/substance/substance.ts +148 -0
- package/src/testing/drift.ts +28 -0
- package/src/transport/contacts.ts +56 -0
- package/src/transport/radiation.ts +238 -0
- package/src/transport/world.ts +437 -0
- package/src/world/fingerprint.ts +106 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a reaction is: stoichiometry, a rate law, and what kind of change it represents.
|
|
3
|
+
*
|
|
4
|
+
* **No enthalpy field**, and that is the same decision `SpeciesDefinition` makes about molar mass.
|
|
5
|
+
* A reaction's `ΔH` is derived from its species' formation enthalpies by Hess's law at
|
|
6
|
+
* registration, so a number that disagreed with its own species cannot exist. `§5` of the design is
|
|
7
|
+
* the argument, and CH-0's heating-value test is what makes the underlying numbers trustworthy.
|
|
8
|
+
*
|
|
9
|
+
* **No gates yet.** The design's `ReactionGates` — a minimum oxygen fraction, a phase requirement,
|
|
10
|
+
* a moisture ceiling — are refused in this phase rather than stubbed: an oxygen gate without a gas
|
|
11
|
+
* field to read is a constant pretending to be physics, and the field is CH-4. The network here
|
|
12
|
+
* runs on mass action alone.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const KIND_DRYING = 0;
|
|
16
|
+
export const KIND_PYROLYSIS = 1;
|
|
17
|
+
export const KIND_COMBUSTION = 2;
|
|
18
|
+
export const KIND_CHAR_OXIDATION = 3;
|
|
19
|
+
export const KIND_CALCINATION = 4;
|
|
20
|
+
export const KIND_CORROSION = 5;
|
|
21
|
+
export const KIND_DISSOLUTION = 6;
|
|
22
|
+
export const KIND_DENATURATION = 7;
|
|
23
|
+
export const KIND_BROWNING = 8;
|
|
24
|
+
export const KIND_BIOLOGICAL = 9;
|
|
25
|
+
export const KIND_PHASE_CHANGE = 10;
|
|
26
|
+
|
|
27
|
+
export type ReactionKind =
|
|
28
|
+
| typeof KIND_DRYING
|
|
29
|
+
| typeof KIND_PYROLYSIS
|
|
30
|
+
| typeof KIND_COMBUSTION
|
|
31
|
+
| typeof KIND_CHAR_OXIDATION
|
|
32
|
+
| typeof KIND_CALCINATION
|
|
33
|
+
| typeof KIND_CORROSION
|
|
34
|
+
| typeof KIND_DISSOLUTION
|
|
35
|
+
| typeof KIND_DENATURATION
|
|
36
|
+
| typeof KIND_BROWNING
|
|
37
|
+
| typeof KIND_BIOLOGICAL
|
|
38
|
+
| typeof KIND_PHASE_CHANGE;
|
|
39
|
+
|
|
40
|
+
export interface SpeciesAmount {
|
|
41
|
+
readonly species: string;
|
|
42
|
+
/** Moles per unit of reaction extent. Fractional is allowed and often necessary. */
|
|
43
|
+
readonly moles: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* `k(T) = A · T^n · exp(−Ea / R·T)`. Monotonic in temperature, which is right for everything a
|
|
48
|
+
* flame does and wrong for everything an enzyme does.
|
|
49
|
+
*/
|
|
50
|
+
export interface ArrheniusKinetics {
|
|
51
|
+
readonly type: 'arrhenius';
|
|
52
|
+
readonly A: number;
|
|
53
|
+
/** J/mol. */
|
|
54
|
+
readonly activationEnergy: number;
|
|
55
|
+
readonly n?: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A peaked curve: zero below `minimum`, one at `optimum`, zero above `maximum`.
|
|
60
|
+
*
|
|
61
|
+
* **Enzyme-catalysed rates are not Arrhenius**, and using one gives compost that heats forever.
|
|
62
|
+
* `§8.6` is the argument; this is where the shape lives.
|
|
63
|
+
*/
|
|
64
|
+
export interface CardinalKinetics {
|
|
65
|
+
readonly type: 'cardinal';
|
|
66
|
+
/** The rate at the optimum. */
|
|
67
|
+
readonly peak: number;
|
|
68
|
+
readonly minimum: number;
|
|
69
|
+
readonly optimum: number;
|
|
70
|
+
readonly maximum: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Arrhenius, capped by how fast the reactant can physically reach the surface.
|
|
75
|
+
*
|
|
76
|
+
* **This is what makes char glow for an hour instead of vanishing in seconds, and `§7` says why.**
|
|
77
|
+
* Above about 800 K the chemistry of carbon oxidation outruns oxygen delivery by an order of
|
|
78
|
+
* magnitude, so what you are actually watching is transport. The kinetic rate alone gives char that
|
|
79
|
+
* disappears; the cap gives char that sits there glowing, which is what char does.
|
|
80
|
+
*
|
|
81
|
+
* The two combine as a **series resistance** — `1/k = 1/k_kinetic + 1/k_transport` — which is the
|
|
82
|
+
* same arithmetic a shell boundary uses for the same reason, and it means neither side has to know
|
|
83
|
+
* about the other. And the transport half rises with the gas moving past, so **blowing on embers
|
|
84
|
+
* makes them brighter and stops when you stop**.
|
|
85
|
+
*/
|
|
86
|
+
export interface DiffusionKinetics {
|
|
87
|
+
readonly type: 'diffusion';
|
|
88
|
+
readonly A: number;
|
|
89
|
+
/** J/mol. */
|
|
90
|
+
readonly activationEnergy: number;
|
|
91
|
+
readonly n?: number;
|
|
92
|
+
/** The ceiling transport puts on it in still air, per second. */
|
|
93
|
+
readonly transportLimit: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** As fast as the reactants allow: the clamp in the solver is the only thing limiting it. */
|
|
97
|
+
export interface InstantKinetics {
|
|
98
|
+
readonly type: 'instant';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type Kinetics = ArrheniusKinetics | CardinalKinetics | DiffusionKinetics | InstantKinetics;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* When a reaction is allowed to run at all.
|
|
105
|
+
*
|
|
106
|
+
* Separate from the rate law because it is a different kind of statement: kinetics say how fast,
|
|
107
|
+
* a gate says whether. Ice does not melt at all below 273.15 K, and expressing that as a very small
|
|
108
|
+
* Arrhenius rate would be a lie that leaks a trickle of water into every freezer.
|
|
109
|
+
*/
|
|
110
|
+
export interface ReactionGates {
|
|
111
|
+
/** K. Below this, nothing. */
|
|
112
|
+
readonly minTemperature?: number;
|
|
113
|
+
/** K. Above this, nothing. */
|
|
114
|
+
readonly maxTemperature?: number;
|
|
115
|
+
/**
|
|
116
|
+
* K over which a gate opens rather than snapping: upward from `minTemperature`, and downward from
|
|
117
|
+
* `maxTemperature` where there is one.
|
|
118
|
+
*
|
|
119
|
+
* Zero for a pure substance. A few kelvin for a mixture: butter softens over about three, because
|
|
120
|
+
* it is triglycerides with different melting points, and that band is what makes fat render
|
|
121
|
+
* slowly instead of switching from solid to liquid.
|
|
122
|
+
*
|
|
123
|
+
* **One number for both ends**, because no reaction here wants a sharp ceiling over a soft floor
|
|
124
|
+
* and a second field would be a knob nobody turns. A reaction with both gates set tapers
|
|
125
|
+
* symmetrically and is fully open only between them.
|
|
126
|
+
*/
|
|
127
|
+
readonly width?: number;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface Reaction {
|
|
131
|
+
readonly id: string;
|
|
132
|
+
readonly reactants: readonly SpeciesAmount[];
|
|
133
|
+
readonly products: readonly SpeciesAmount[];
|
|
134
|
+
readonly kinetics: Kinetics;
|
|
135
|
+
readonly kind: ReactionKind;
|
|
136
|
+
readonly gates?: ReactionGates;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* A phase change, as the reaction it actually is.
|
|
141
|
+
*
|
|
142
|
+
* **Its enthalpy is not given here and must not be**: Hess's law over CH-0's formation enthalpies
|
|
143
|
+
* gives 333,611 J/kg for ice melting against a literature 333,550, and — the part that settles the
|
|
144
|
+
* design — 2,442,631 J/kg for water boiling, which is the value **at 298.15 K** rather than the
|
|
145
|
+
* 2,256,400 quoted at the boiling point. That is right, and it means the latent heat at any
|
|
146
|
+
* temperature stops being stored and becomes a consequence of the heat capacities either side,
|
|
147
|
+
* varying the way the real one does.
|
|
148
|
+
*
|
|
149
|
+
* **A fast finite rate rather than `Instant`, and the difference is the whole plateau.** Instant
|
|
150
|
+
* kinetics would let the clamp convert every gram of water it could reach in a single sub-step —
|
|
151
|
+
* two and a half megajoules absorbed at once, and a temperature that falls hundreds of kelvin below
|
|
152
|
+
* the boiling point rather than sitting on it. A finite rate through an opening gate is a
|
|
153
|
+
* proportional controller instead: the further above the transition the temperature strays, the
|
|
154
|
+
* more converts, the more heat is absorbed, and it settles just above with an overshoot
|
|
155
|
+
* proportional to the flux arriving. That settling *is* the plateau, and it comes out of the
|
|
156
|
+
* substep-and-clamp solver rather than out of a table.
|
|
157
|
+
*
|
|
158
|
+
* `activationEnergy: 0` makes the Arrhenius form a constant, so no second kinetics kind was needed.
|
|
159
|
+
*/
|
|
160
|
+
export function phaseChange(
|
|
161
|
+
id: string,
|
|
162
|
+
from: string,
|
|
163
|
+
to: string,
|
|
164
|
+
temperature: number,
|
|
165
|
+
width = PHASE_GATE_WIDTH,
|
|
166
|
+
rate = PHASE_RATE,
|
|
167
|
+
): Reaction {
|
|
168
|
+
return {
|
|
169
|
+
id,
|
|
170
|
+
reactants: [{ species: from, moles: 1 }],
|
|
171
|
+
products: [{ species: to, moles: 1 }],
|
|
172
|
+
kinetics: { type: 'arrhenius', A: rate, activationEnergy: 0 },
|
|
173
|
+
kind: KIND_PHASE_CHANGE,
|
|
174
|
+
gates: { minTemperature: temperature, width },
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* A phase change the other way up: the one that runs as something *cools*.
|
|
180
|
+
*
|
|
181
|
+
* **`phaseChange` is "heat arrives" and this is "heat leaves", and the pair is not symmetric by
|
|
182
|
+
* accident.** Everything the library had until now was the first kind — ice melts, water boils, wax
|
|
183
|
+
* vaporises — because a fire is heat arriving. A vapour condensing is the same mechanism read
|
|
184
|
+
* backwards: it runs *below* a temperature rather than above one, its gate closes as the
|
|
185
|
+
* temperature rises through the transition, and its enthalpy comes out with the opposite sign
|
|
186
|
+
* because Hess's law over the two species already carries it.
|
|
187
|
+
*
|
|
188
|
+
* **The gate closes over `width` rather than snapping**, and `gateOpening` grew the upper taper for
|
|
189
|
+
* this. Without it the pair rings: the temperature falls through the transition, the gate opens
|
|
190
|
+
* fully, latent heat is released, the temperature rises back through it, and the gate slams. That
|
|
191
|
+
* is `PHASE_GATE_WIDTH`'s argument with the signs reversed, so it takes the same answer.
|
|
192
|
+
*
|
|
193
|
+
* **A condensation wants a reverse reaction and its caller must write one.** Nothing here pairs
|
|
194
|
+
* them, because a pair is two entries in a network and the network is data. A condensation with no
|
|
195
|
+
* evaporation is a one-way sink: droplets that drift back into a flame stay droplets, and the smoke
|
|
196
|
+
* over a fire never turns black again.
|
|
197
|
+
*/
|
|
198
|
+
export function condensation(
|
|
199
|
+
id: string,
|
|
200
|
+
from: string,
|
|
201
|
+
to: string,
|
|
202
|
+
temperature: number,
|
|
203
|
+
width = PHASE_GATE_WIDTH,
|
|
204
|
+
rate = PHASE_RATE,
|
|
205
|
+
): Reaction {
|
|
206
|
+
return {
|
|
207
|
+
id,
|
|
208
|
+
reactants: [{ species: from, moles: 1 }],
|
|
209
|
+
products: [{ species: to, moles: 1 }],
|
|
210
|
+
kinetics: { type: 'arrhenius', A: rate, activationEnergy: 0 },
|
|
211
|
+
kind: KIND_PHASE_CHANGE,
|
|
212
|
+
gates: { maxTemperature: temperature, width },
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* How fast a phase change runs with its gate fully open, per second.
|
|
218
|
+
*
|
|
219
|
+
* **A gain, not a physical constant, and it is bounded from both sides.** Too high and one step
|
|
220
|
+
* converts more than the arriving heat can pay for, so the temperature undershoots and the pair
|
|
221
|
+
* rings. Too low and the phase change cannot keep up with a real flux — at this value a kilogram
|
|
222
|
+
* boils at up to half a gram a second per kelvin of overshoot, which is about 1.2 kW and rises with
|
|
223
|
+
* the overshoot, so a kettle settles a thousandth of a kelvin above its boiling point and a blowtorch
|
|
224
|
+
* settles further above it. That the band widens with the flux is correct rather than a compromise.
|
|
225
|
+
*/
|
|
226
|
+
export const PHASE_RATE = 0.5;
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* K over which a phase change's gate opens, for a pure substance.
|
|
230
|
+
*
|
|
231
|
+
* Not zero, and that is what keeps the plateau a plateau rather than a sawtooth: a gate that snapped
|
|
232
|
+
* fully open a hair above the transition would convert far more than the arriving heat can pay for,
|
|
233
|
+
* undershoot, snap shut, and ring. Half a kelvin is the band the temperature is then held within.
|
|
234
|
+
*/
|
|
235
|
+
export const PHASE_GATE_WIDTH = 0.5;
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The reactions a world knows about, balanced and costed at registration.
|
|
3
|
+
*
|
|
4
|
+
* Two things happen here that cannot happen later, and both are refusals rather than warnings.
|
|
5
|
+
*
|
|
6
|
+
* **Every reaction is checked against the element matrix**, and one that does not balance is
|
|
7
|
+
* refused naming the element and the size of the gap. An unbalanced reaction invents or destroys
|
|
8
|
+
* matter every time it runs, and every symptom of that is subtle — a fire that will not go out, a
|
|
9
|
+
* mass that quietly grows, smoke that never clears. CH-0 built the matrix for exactly this moment.
|
|
10
|
+
*
|
|
11
|
+
* **Every reaction's enthalpy is derived by Hess's law** from the formation enthalpies CH-0 stores,
|
|
12
|
+
* rather than typed in. A `ΔH` that disagreed with its own species would conserve energy in the
|
|
13
|
+
* code and lose it against the world, with nothing to detect it. Derived, the two cannot disagree,
|
|
14
|
+
* and a wrong formation enthalpy shows up in every reaction that touches the species at once —
|
|
15
|
+
* which is a bug that can be found.
|
|
16
|
+
*/
|
|
17
|
+
import { ELEMENT_COUNT, ELEMENTS } from '../element/elements.ts';
|
|
18
|
+
import type { SpeciesRegistry } from '../species/registry.ts';
|
|
19
|
+
import { type RateTable, tabulateArrhenius, tabulateCardinal } from './rates.ts';
|
|
20
|
+
import type { Reaction, ReactionKind, SpeciesAmount } from './reaction.ts';
|
|
21
|
+
|
|
22
|
+
export * from './reaction.ts';
|
|
23
|
+
|
|
24
|
+
/** How far a reaction may be from balancing, in moles of an element per unit extent. */
|
|
25
|
+
const BALANCE_TOLERANCE = 1e-9;
|
|
26
|
+
|
|
27
|
+
/** `instant` has no table; the solver's clamp is what bounds it. A huge constant stands in. */
|
|
28
|
+
const INSTANT_RATE = 1e12;
|
|
29
|
+
|
|
30
|
+
export class ReactionRegistry {
|
|
31
|
+
private readonly ids: string[] = [];
|
|
32
|
+
private readonly byId = new Map<string, number>();
|
|
33
|
+
private readonly kinds: ReactionKind[] = [];
|
|
34
|
+
private readonly gateMin: number[] = [];
|
|
35
|
+
private readonly gateMax: number[] = [];
|
|
36
|
+
private readonly gateWidth: number[] = [];
|
|
37
|
+
private readonly transport: number[] = [];
|
|
38
|
+
private readonly enthalpies: number[] = [];
|
|
39
|
+
private readonly tables: (RateTable | null)[] = [];
|
|
40
|
+
private readonly reactantIndex: Int32Array[] = [];
|
|
41
|
+
private readonly reactantAmount: Float64Array[] = [];
|
|
42
|
+
private readonly productIndex: Int32Array[] = [];
|
|
43
|
+
private readonly productAmount: Float64Array[] = [];
|
|
44
|
+
|
|
45
|
+
/* Scratch for the balance check. Registration-time, so this is tidiness rather than a hot path. */
|
|
46
|
+
private readonly balance = new Float64Array(ELEMENT_COUNT);
|
|
47
|
+
|
|
48
|
+
constructor(private readonly species: SpeciesRegistry) {}
|
|
49
|
+
|
|
50
|
+
get count(): number {
|
|
51
|
+
return this.ids.length;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
register(reaction: Reaction): number {
|
|
55
|
+
const { id } = reaction;
|
|
56
|
+
if (this.byId.has(id)) throw new Error(`reaction "${id}" is already registered`);
|
|
57
|
+
if (reaction.reactants.length === 0) throw new Error(`reaction "${id}" has no reactants`);
|
|
58
|
+
if (reaction.products.length === 0) throw new Error(`reaction "${id}" has no products`);
|
|
59
|
+
|
|
60
|
+
const resolve = (side: readonly SpeciesAmount[], what: string): [Int32Array, Float64Array] => {
|
|
61
|
+
const index = new Int32Array(side.length);
|
|
62
|
+
const amount = new Float64Array(side.length);
|
|
63
|
+
for (let i = 0; i < side.length; i++) {
|
|
64
|
+
const entry = side[i] as SpeciesAmount;
|
|
65
|
+
const at = this.species.indexOf(entry.species);
|
|
66
|
+
if (at < 0) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`reaction "${id}" names ${what} "${entry.species}", which is not a registered species`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
if (!Number.isFinite(entry.moles) || entry.moles <= 0) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`reaction "${id}" gives ${what} "${entry.species}" a coefficient of ${entry.moles}`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
index[i] = at;
|
|
77
|
+
amount[i] = entry.moles;
|
|
78
|
+
}
|
|
79
|
+
return [index, amount];
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const [reactantIndex, reactantAmount] = resolve(reaction.reactants, 'reactant');
|
|
83
|
+
const [productIndex, productAmount] = resolve(reaction.products, 'product');
|
|
84
|
+
|
|
85
|
+
/* Products minus reactants, element by element. Zero everywhere or it does not run. */
|
|
86
|
+
this.balance.fill(0);
|
|
87
|
+
for (let i = 0; i < productIndex.length; i++) {
|
|
88
|
+
this.species.elements.addScaledRow(
|
|
89
|
+
productIndex[i] as number,
|
|
90
|
+
productAmount[i] as number,
|
|
91
|
+
this.balance,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
for (let i = 0; i < reactantIndex.length; i++) {
|
|
95
|
+
this.species.elements.addScaledRow(
|
|
96
|
+
reactantIndex[i] as number,
|
|
97
|
+
-(reactantAmount[i] as number),
|
|
98
|
+
this.balance,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
for (let e = 0; e < ELEMENT_COUNT; e++) {
|
|
102
|
+
const gap = this.balance[e] as number;
|
|
103
|
+
if (Math.abs(gap) > BALANCE_TOLERANCE) {
|
|
104
|
+
const side = gap > 0 ? 'more' : 'less';
|
|
105
|
+
throw new Error(
|
|
106
|
+
`reaction "${id}" does not balance: its products hold ${Math.abs(gap)} mol ${side} ` +
|
|
107
|
+
`${ELEMENTS[e]} than its reactants`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* Hess's law: the products' formation enthalpies less the reactants'. */
|
|
113
|
+
let enthalpy = 0;
|
|
114
|
+
for (let i = 0; i < productIndex.length; i++) {
|
|
115
|
+
enthalpy +=
|
|
116
|
+
(productAmount[i] as number) * this.species.formationEnthalpyOf(productIndex[i] as number);
|
|
117
|
+
}
|
|
118
|
+
for (let i = 0; i < reactantIndex.length; i++) {
|
|
119
|
+
enthalpy -=
|
|
120
|
+
(reactantAmount[i] as number) *
|
|
121
|
+
this.species.formationEnthalpyOf(reactantIndex[i] as number);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const kinetics = reaction.kinetics;
|
|
125
|
+
let table: RateTable | null;
|
|
126
|
+
let transportLimit = 0;
|
|
127
|
+
switch (kinetics.type) {
|
|
128
|
+
case 'arrhenius':
|
|
129
|
+
table = tabulateArrhenius(kinetics.A, kinetics.activationEnergy, kinetics.n ?? 0);
|
|
130
|
+
break;
|
|
131
|
+
case 'diffusion':
|
|
132
|
+
table = tabulateArrhenius(kinetics.A, kinetics.activationEnergy, kinetics.n ?? 0);
|
|
133
|
+
transportLimit = kinetics.transportLimit;
|
|
134
|
+
if (!(transportLimit > 0)) {
|
|
135
|
+
throw new Error(`reaction "${id}" is transport-limited at ${transportLimit} per second`);
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
case 'cardinal':
|
|
139
|
+
table = tabulateCardinal(
|
|
140
|
+
kinetics.peak,
|
|
141
|
+
kinetics.minimum,
|
|
142
|
+
kinetics.optimum,
|
|
143
|
+
kinetics.maximum,
|
|
144
|
+
);
|
|
145
|
+
break;
|
|
146
|
+
case 'instant':
|
|
147
|
+
table = null;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const index = this.ids.length;
|
|
152
|
+
this.ids.push(id);
|
|
153
|
+
this.byId.set(id, index);
|
|
154
|
+
this.kinds.push(reaction.kind);
|
|
155
|
+
const gates = reaction.gates;
|
|
156
|
+
this.gateMin.push(gates?.minTemperature ?? Number.NEGATIVE_INFINITY);
|
|
157
|
+
this.gateMax.push(gates?.maxTemperature ?? Number.POSITIVE_INFINITY);
|
|
158
|
+
this.gateWidth.push(gates?.width ?? 0);
|
|
159
|
+
this.enthalpies.push(enthalpy);
|
|
160
|
+
this.tables.push(table);
|
|
161
|
+
this.transport.push(transportLimit);
|
|
162
|
+
this.reactantIndex.push(reactantIndex);
|
|
163
|
+
this.reactantAmount.push(reactantAmount);
|
|
164
|
+
this.productIndex.push(productIndex);
|
|
165
|
+
this.productAmount.push(productAmount);
|
|
166
|
+
return index;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
indexOf(id: string): number {
|
|
170
|
+
return this.byId.get(id) ?? -1;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
idOf(reaction: number): string {
|
|
174
|
+
return this.ids[this.check(reaction)] as string;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
kindOf(reaction: number): ReactionKind {
|
|
178
|
+
return this.kinds[this.check(reaction)] as ReactionKind;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** J per unit extent. Negative releases heat; positive absorbs it. */
|
|
182
|
+
enthalpyOf(reaction: number): number {
|
|
183
|
+
return this.enthalpies[this.check(reaction)] as number;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
rateTableOf(reaction: number): RateTable {
|
|
187
|
+
const table = this.tables[this.check(reaction)];
|
|
188
|
+
if (table === null) {
|
|
189
|
+
throw new Error(`reaction "${this.ids[reaction]}" is instant and has no rate table`);
|
|
190
|
+
}
|
|
191
|
+
return table;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** K below which this reaction does not run at all. */
|
|
195
|
+
minTemperatureOf(reaction: number): number {
|
|
196
|
+
return this.gateMin[this.check(reaction)] as number;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** K above which it does not run at all. */
|
|
200
|
+
maxTemperatureOf(reaction: number): number {
|
|
201
|
+
return this.gateMax[this.check(reaction)] as number;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** K over which the lower gate opens rather than snapping. */
|
|
205
|
+
gateWidthOf(reaction: number): number {
|
|
206
|
+
return this.gateWidth[this.check(reaction)] as number;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** The ceiling transport puts on this reaction in still air, per second. Zero means none. */
|
|
210
|
+
transportLimitOf(reaction: number): number {
|
|
211
|
+
return this.transport[this.check(reaction)] as number;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** The table, or `null` where the reaction is instant. What the localiser packs. */
|
|
215
|
+
rateTableOrNull(reaction: number): RateTable | null {
|
|
216
|
+
return this.tables[this.check(reaction)] as RateTable | null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** The rate constant at a temperature. An instant reaction answers with a very large number. */
|
|
220
|
+
rateOf(reaction: number, temperature: number): number {
|
|
221
|
+
const table = this.tables[this.check(reaction)];
|
|
222
|
+
if (table === null) return INSTANT_RATE;
|
|
223
|
+
if (temperature <= table.minTemperature) return table.rate[table.knots - 1] as number;
|
|
224
|
+
if (temperature >= table.maxTemperature) return table.rate[0] as number;
|
|
225
|
+
const position =
|
|
226
|
+
((1 / temperature - table.inverseMin) / (table.inverseMax - table.inverseMin)) *
|
|
227
|
+
(table.knots - 1);
|
|
228
|
+
const low = Math.floor(position);
|
|
229
|
+
const high = low + 1;
|
|
230
|
+
if (high >= table.knots) return table.rate[table.knots - 1] as number;
|
|
231
|
+
const at = position - low;
|
|
232
|
+
return (table.rate[low] as number) * (1 - at) + (table.rate[high] as number) * at;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
reactantSpecies(reaction: number): Int32Array {
|
|
236
|
+
return this.reactantIndex[this.check(reaction)] as Int32Array;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
reactantMoles(reaction: number): Float64Array {
|
|
240
|
+
return this.reactantAmount[this.check(reaction)] as Float64Array;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
productSpecies(reaction: number): Int32Array {
|
|
244
|
+
return this.productIndex[this.check(reaction)] as Int32Array;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
productMoles(reaction: number): Float64Array {
|
|
248
|
+
return this.productAmount[this.check(reaction)] as Float64Array;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private check(reaction: number): number {
|
|
252
|
+
if (!Number.isInteger(reaction) || reaction < 0 || reaction >= this.ids.length) {
|
|
253
|
+
throw new Error(`reaction ${reaction} is outside a registry of ${this.ids.length}`);
|
|
254
|
+
}
|
|
255
|
+
return reaction;
|
|
256
|
+
}
|
|
257
|
+
}
|