@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,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The closed element set, and the atomic weights every molar mass is derived from.
|
|
3
|
+
*
|
|
4
|
+
* Fifteen elements, and the set is closed deliberately rather than for want of typing. Every
|
|
5
|
+
* species this package can hold is built from these, so the species-by-element matrix has a fixed
|
|
6
|
+
* width, `elementTotals` reduces into a fixed-length array, and the conservation assertion that
|
|
7
|
+
* `§6` of the design calls the most valuable test in the track is a comparison of two vectors of
|
|
8
|
+
* fifteen doubles rather than a walk over a dictionary.
|
|
9
|
+
*
|
|
10
|
+
* **Adding a sixteenth is a deliberate act with a cost**: every `Float64Array` sized by
|
|
11
|
+
* `ELEMENT_COUNT` grows, and a stored conservation baseline taken before the change no longer
|
|
12
|
+
* compares. That is the right amount of friction for a decision that widens the model's universe.
|
|
13
|
+
*
|
|
14
|
+
* The values are IUPAC conventional atomic weights in g/mol. They are *conventional* rather than
|
|
15
|
+
* the interval form the tables now publish, because a molar mass is arithmetic here and an interval
|
|
16
|
+
* would make every derived mass an interval too — which is honest and useless.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The elements, in the order the design lists them: organic first, then the rest.
|
|
21
|
+
*
|
|
22
|
+
* The order is the wire format of every matrix row and every totals vector in this package, so it
|
|
23
|
+
* is frozen. Reordering it invalidates any baseline a consumer has stored, in exactly the way
|
|
24
|
+
* `AGENTS.md` says the RNG sequence is frozen.
|
|
25
|
+
*/
|
|
26
|
+
export const ELEMENTS = [
|
|
27
|
+
'C',
|
|
28
|
+
'H',
|
|
29
|
+
'O',
|
|
30
|
+
'N',
|
|
31
|
+
'S',
|
|
32
|
+
'Cl',
|
|
33
|
+
'Si',
|
|
34
|
+
'Ca',
|
|
35
|
+
'Fe',
|
|
36
|
+
'Al',
|
|
37
|
+
'Mg',
|
|
38
|
+
'Cu',
|
|
39
|
+
'Na',
|
|
40
|
+
'K',
|
|
41
|
+
'P',
|
|
42
|
+
] as const;
|
|
43
|
+
|
|
44
|
+
export type ElementSymbol = (typeof ELEMENTS)[number];
|
|
45
|
+
|
|
46
|
+
export const ELEMENT_COUNT = ELEMENTS.length;
|
|
47
|
+
|
|
48
|
+
/** Conventional atomic weights, g/mol, indexed the way `ELEMENTS` is. */
|
|
49
|
+
export const ATOMIC_MASS = new Float64Array([
|
|
50
|
+
12.011, // C
|
|
51
|
+
1.008, // H
|
|
52
|
+
15.999, // O
|
|
53
|
+
14.007, // N
|
|
54
|
+
32.06, // S
|
|
55
|
+
35.45, // Cl
|
|
56
|
+
28.085, // Si
|
|
57
|
+
40.078, // Ca
|
|
58
|
+
55.845, // Fe
|
|
59
|
+
26.9815384, // Al
|
|
60
|
+
24.305, // Mg
|
|
61
|
+
63.546, // Cu
|
|
62
|
+
22.98976928, // Na
|
|
63
|
+
39.0983, // K
|
|
64
|
+
30.973761998, // P
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
/*
|
|
68
|
+
* A map rather than an `indexOf`, and case-sensitive rather than folded.
|
|
69
|
+
*
|
|
70
|
+
* Case is load-bearing in chemical notation: `Co` is cobalt and `CO` is carbon monoxide, so a
|
|
71
|
+
* lookup that folded case would accept a molecule where an element was wanted and silently produce
|
|
72
|
+
* a molar mass for the wrong thing. Nothing here runs in a frame, so the map is for legibility
|
|
73
|
+
* rather than speed.
|
|
74
|
+
*/
|
|
75
|
+
const INDEX = new Map<string, number>(ELEMENTS.map((symbol, at) => [symbol, at]));
|
|
76
|
+
|
|
77
|
+
/** The index of an element symbol, or `-1` for anything this package does not carry. */
|
|
78
|
+
export function elementIndex(symbol: string): number {
|
|
79
|
+
return INDEX.get(symbol) ?? -1;
|
|
80
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Moles of each element per mole of each species, as one growable table.
|
|
3
|
+
*
|
|
4
|
+
* This is the law `§6` of the design rests on. A reaction balances when the sum of its reactants'
|
|
5
|
+
* rows equals the sum of its products'; a world conserves matter when the sum of every parcel's
|
|
6
|
+
* composition through this table is constant. Both are one reduction over one `Float64Array`, and
|
|
7
|
+
* that is only true because `ELEMENT_COUNT` is fixed and the storage is dense.
|
|
8
|
+
*
|
|
9
|
+
* **Dense rather than sparse, and it is not close.** Sixty species over fifteen elements is 900
|
|
10
|
+
* doubles, or 7.2 KB. A sparse structure would save perhaps two thirds of that and cost an
|
|
11
|
+
* indirection in the innermost loop of every conservation check. The dense table is smaller than
|
|
12
|
+
* one texture tile.
|
|
13
|
+
*
|
|
14
|
+
* Row-major, species-major: a species' fifteen elements are adjacent, because every consumer of
|
|
15
|
+
* this table walks one species at a time.
|
|
16
|
+
*/
|
|
17
|
+
import { ELEMENT_COUNT } from './elements.ts';
|
|
18
|
+
|
|
19
|
+
/** Rows the table holds before its first growth. Doubles from here. */
|
|
20
|
+
const INITIAL_ROWS = 64;
|
|
21
|
+
|
|
22
|
+
export class SpeciesElementMatrix {
|
|
23
|
+
private store = new Float64Array(INITIAL_ROWS * ELEMENT_COUNT);
|
|
24
|
+
private used = 0;
|
|
25
|
+
|
|
26
|
+
get rows(): number {
|
|
27
|
+
return this.used;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Append a zeroed row and return its index. */
|
|
31
|
+
addRow(): number {
|
|
32
|
+
if ((this.used + 1) * ELEMENT_COUNT > this.store.length) {
|
|
33
|
+
const grown = new Float64Array(this.store.length * 2);
|
|
34
|
+
grown.set(this.store);
|
|
35
|
+
this.store = grown;
|
|
36
|
+
}
|
|
37
|
+
return this.used++;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
set(row: number, element: number, moles: number): void {
|
|
41
|
+
this.check(row, element);
|
|
42
|
+
this.store[row * ELEMENT_COUNT + element] = moles;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
at(row: number, element: number): number {
|
|
46
|
+
this.check(row, element);
|
|
47
|
+
return this.store[row * ELEMENT_COUNT + element] as number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A row as a view of fifteen, sharing storage rather than copying.
|
|
52
|
+
*
|
|
53
|
+
* **A view is invalidated by a later `addRow` that grows the table**, because growth reallocates.
|
|
54
|
+
* Every caller in this package takes a view inside a loop that registers nothing, which is the
|
|
55
|
+
* only safe pattern and is why this is not handed out as a promise.
|
|
56
|
+
*/
|
|
57
|
+
rowView(row: number): Float64Array {
|
|
58
|
+
this.check(row, 0);
|
|
59
|
+
return this.store.subarray(row * ELEMENT_COUNT, (row + 1) * ELEMENT_COUNT);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* `out[e] += scale * this[row][e]`, for all fifteen. Allocates nothing.
|
|
64
|
+
*
|
|
65
|
+
* The primitive every conservation check is built from, and it exists rather than a loop over
|
|
66
|
+
* `at` for one reason: `rowView` would be the natural way to write that loop and `subarray`
|
|
67
|
+
* allocates a view object per call, which is an allocation per species per tick. It adds rather
|
|
68
|
+
* than replaces so a caller sums a whole world into one vector without a second accumulator.
|
|
69
|
+
*/
|
|
70
|
+
addScaledRow(row: number, scale: number, out: Float64Array): void {
|
|
71
|
+
this.check(row, 0);
|
|
72
|
+
if (out.length !== ELEMENT_COUNT) {
|
|
73
|
+
throw new Error(`a totals vector is ${ELEMENT_COUNT} wide, not ${out.length}`);
|
|
74
|
+
}
|
|
75
|
+
const base = row * ELEMENT_COUNT;
|
|
76
|
+
for (let e = 0; e < ELEMENT_COUNT; e++) {
|
|
77
|
+
out[e] = (out[e] as number) + scale * (this.store[base + e] as number);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private check(row: number, element: number): void {
|
|
82
|
+
if (!Number.isInteger(row) || row < 0 || row >= this.used) {
|
|
83
|
+
throw new Error(`species row ${row} is outside a matrix of ${this.used}`);
|
|
84
|
+
}
|
|
85
|
+
if (!Number.isInteger(element) || element < 0 || element >= ELEMENT_COUNT) {
|
|
86
|
+
throw new Error(`element ${element} is outside the set of ${ELEMENT_COUNT}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The far field: what the air is where nothing has happened to it.
|
|
3
|
+
*
|
|
4
|
+
* A consumer's constant, and everything downstream adjusts to it without a feature. A low-oxygen
|
|
5
|
+
* atmosphere makes fire hard to start and easy to put out; a high-oxygen one makes everything
|
|
6
|
+
* terrifying; a thin one lowers every boiling point in the world. None of those needed code.
|
|
7
|
+
*/
|
|
8
|
+
import type { SpeciesRegistry } from '../species/registry.ts';
|
|
9
|
+
|
|
10
|
+
/** Pa. */
|
|
11
|
+
export const STANDARD_PRESSURE_PA = 101325;
|
|
12
|
+
|
|
13
|
+
/** J/(mol·K). */
|
|
14
|
+
export const GAS_CONSTANT = 8.314462618;
|
|
15
|
+
|
|
16
|
+
export interface AmbientState {
|
|
17
|
+
readonly temperature: number;
|
|
18
|
+
readonly pressure: number;
|
|
19
|
+
/** Relative humidity, 0..1. Added to the dry composition when a cell is filled. */
|
|
20
|
+
readonly humidity: number;
|
|
21
|
+
/** Dry mass fractions over gas species, summing to one. */
|
|
22
|
+
readonly composition: Readonly<Record<string, number>>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Dry air at 20 °C and one atmosphere, at 50% relative humidity.
|
|
27
|
+
*
|
|
28
|
+
* **Argon is lumped into nitrogen**, per the species table's own note: it is 0.93% of air and
|
|
29
|
+
* chemically inert, so all it contributes is heat capacity and dilution. What that costs is
|
|
30
|
+
* measurable and small — the mean molar mass comes out at 28.86 g/mol against air's real 28.96,
|
|
31
|
+
* which is 0.4% — and what it buys is not carrying a sixteenth element for a gas that never reacts.
|
|
32
|
+
*
|
|
33
|
+
* The mass fractions are chosen so the *volume* fraction of oxygen comes back at 20.95%, because
|
|
34
|
+
* that is the number every extinction criterion in `§12` is written against.
|
|
35
|
+
*/
|
|
36
|
+
export const STANDARD_AIR: AmbientState = {
|
|
37
|
+
temperature: 293.15,
|
|
38
|
+
pressure: STANDARD_PRESSURE_PA,
|
|
39
|
+
humidity: 0.5,
|
|
40
|
+
composition: { N2: 0.767074, O2: 0.232285, CO2: 0.000641 },
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** The gas species a field tracks unless a consumer names its own. */
|
|
44
|
+
export const STANDARD_FIELD_SPECIES: readonly string[] = [
|
|
45
|
+
'O2',
|
|
46
|
+
'N2',
|
|
47
|
+
'CO2',
|
|
48
|
+
'H2O(g)',
|
|
49
|
+
'CO',
|
|
50
|
+
'CH4',
|
|
51
|
+
'H2',
|
|
52
|
+
'C2H4',
|
|
53
|
+
'NH3',
|
|
54
|
+
'HCN',
|
|
55
|
+
'HCl',
|
|
56
|
+
'SO2',
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Saturation vapour pressure of water, Pa, by the Magnus form.
|
|
61
|
+
*
|
|
62
|
+
* Authoring-time: it calls `exp`, and `§7` keeps transcendentals off the tick. A field fills a new
|
|
63
|
+
* cell from the ambient, which is a discrete event rather than a per-tick one, and CH-5's
|
|
64
|
+
* evaporation will need this tabulated the way every rate already is.
|
|
65
|
+
*/
|
|
66
|
+
export function saturationVapourPressure(temperature: number): number {
|
|
67
|
+
const celsius = temperature - 273.15;
|
|
68
|
+
// determinism: build-time — a field fills a new cell from the ambient, which is a discrete event
|
|
69
|
+
return 610.94 * Math.exp((17.625 * celsius) / (celsius + 243.04));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Mass of each tracked species in one cell of ambient air, kg.
|
|
74
|
+
*
|
|
75
|
+
* From the ideal gas law at the ambient temperature and pressure, with the humidity turned into
|
|
76
|
+
* water vapour by its partial pressure. Writes into `out`, one entry per field species.
|
|
77
|
+
*/
|
|
78
|
+
export function ambientCellMass(
|
|
79
|
+
registry: SpeciesRegistry,
|
|
80
|
+
fieldSpecies: Int32Array,
|
|
81
|
+
ambient: AmbientState,
|
|
82
|
+
volume: number,
|
|
83
|
+
out: Float64Array,
|
|
84
|
+
): void {
|
|
85
|
+
const vapourPressure = ambient.humidity * saturationVapourPressure(ambient.temperature);
|
|
86
|
+
const dryPressure = Math.max(0, ambient.pressure - vapourPressure);
|
|
87
|
+
const totalMoles = (ambient.pressure * volume) / (GAS_CONSTANT * ambient.temperature);
|
|
88
|
+
const vapourMoles = (vapourPressure * volume) / (GAS_CONSTANT * ambient.temperature);
|
|
89
|
+
const dryMoles = totalMoles - vapourMoles;
|
|
90
|
+
|
|
91
|
+
/* The dry composition is by mass, so its mean molar mass is what converts moles to kilograms. */
|
|
92
|
+
let inverseMean = 0;
|
|
93
|
+
for (const [id, fraction] of Object.entries(ambient.composition)) {
|
|
94
|
+
const species = registry.indexOf(id);
|
|
95
|
+
if (species < 0) throw new Error(`ambient names "${id}", which is not a registered species`);
|
|
96
|
+
inverseMean += fraction / registry.molarMass(species);
|
|
97
|
+
}
|
|
98
|
+
const meanMolarMass = 1 / inverseMean;
|
|
99
|
+
const dryMass = dryMoles * meanMolarMass;
|
|
100
|
+
|
|
101
|
+
out.fill(0);
|
|
102
|
+
for (let i = 0; i < fieldSpecies.length; i++) {
|
|
103
|
+
const id = registry.idOf(fieldSpecies[i] as number);
|
|
104
|
+
const fraction = ambient.composition[id];
|
|
105
|
+
if (fraction !== undefined) out[i] = fraction * dryMass;
|
|
106
|
+
if (id === 'H2O(g)') {
|
|
107
|
+
out[i] = (out[i] as number) + vapourMoles * registry.molarMass(fieldSpecies[i] as number);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
void dryPressure;
|
|
111
|
+
}
|