@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
|
+
* 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.js';
|
|
18
|
+
/** Rows the table holds before its first growth. Doubles from here. */
|
|
19
|
+
const INITIAL_ROWS = 64;
|
|
20
|
+
export class SpeciesElementMatrix {
|
|
21
|
+
store = new Float64Array(INITIAL_ROWS * ELEMENT_COUNT);
|
|
22
|
+
used = 0;
|
|
23
|
+
get rows() {
|
|
24
|
+
return this.used;
|
|
25
|
+
}
|
|
26
|
+
/** Append a zeroed row and return its index. */
|
|
27
|
+
addRow() {
|
|
28
|
+
if ((this.used + 1) * ELEMENT_COUNT > this.store.length) {
|
|
29
|
+
const grown = new Float64Array(this.store.length * 2);
|
|
30
|
+
grown.set(this.store);
|
|
31
|
+
this.store = grown;
|
|
32
|
+
}
|
|
33
|
+
return this.used++;
|
|
34
|
+
}
|
|
35
|
+
set(row, element, moles) {
|
|
36
|
+
this.check(row, element);
|
|
37
|
+
this.store[row * ELEMENT_COUNT + element] = moles;
|
|
38
|
+
}
|
|
39
|
+
at(row, element) {
|
|
40
|
+
this.check(row, element);
|
|
41
|
+
return this.store[row * ELEMENT_COUNT + element];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* A row as a view of fifteen, sharing storage rather than copying.
|
|
45
|
+
*
|
|
46
|
+
* **A view is invalidated by a later `addRow` that grows the table**, because growth reallocates.
|
|
47
|
+
* Every caller in this package takes a view inside a loop that registers nothing, which is the
|
|
48
|
+
* only safe pattern and is why this is not handed out as a promise.
|
|
49
|
+
*/
|
|
50
|
+
rowView(row) {
|
|
51
|
+
this.check(row, 0);
|
|
52
|
+
return this.store.subarray(row * ELEMENT_COUNT, (row + 1) * ELEMENT_COUNT);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* `out[e] += scale * this[row][e]`, for all fifteen. Allocates nothing.
|
|
56
|
+
*
|
|
57
|
+
* The primitive every conservation check is built from, and it exists rather than a loop over
|
|
58
|
+
* `at` for one reason: `rowView` would be the natural way to write that loop and `subarray`
|
|
59
|
+
* allocates a view object per call, which is an allocation per species per tick. It adds rather
|
|
60
|
+
* than replaces so a caller sums a whole world into one vector without a second accumulator.
|
|
61
|
+
*/
|
|
62
|
+
addScaledRow(row, scale, out) {
|
|
63
|
+
this.check(row, 0);
|
|
64
|
+
if (out.length !== ELEMENT_COUNT) {
|
|
65
|
+
throw new Error(`a totals vector is ${ELEMENT_COUNT} wide, not ${out.length}`);
|
|
66
|
+
}
|
|
67
|
+
const base = row * ELEMENT_COUNT;
|
|
68
|
+
for (let e = 0; e < ELEMENT_COUNT; e++) {
|
|
69
|
+
out[e] = out[e] + scale * this.store[base + e];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
check(row, element) {
|
|
73
|
+
if (!Number.isInteger(row) || row < 0 || row >= this.used) {
|
|
74
|
+
throw new Error(`species row ${row} is outside a matrix of ${this.used}`);
|
|
75
|
+
}
|
|
76
|
+
if (!Number.isInteger(element) || element < 0 || element >= ELEMENT_COUNT) {
|
|
77
|
+
throw new Error(`element ${element} is outside the set of ${ELEMENT_COUNT}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
/** Pa. */
|
|
10
|
+
export declare const STANDARD_PRESSURE_PA = 101325;
|
|
11
|
+
/** J/(mol·K). */
|
|
12
|
+
export declare const GAS_CONSTANT = 8.314462618;
|
|
13
|
+
export interface AmbientState {
|
|
14
|
+
readonly temperature: number;
|
|
15
|
+
readonly pressure: number;
|
|
16
|
+
/** Relative humidity, 0..1. Added to the dry composition when a cell is filled. */
|
|
17
|
+
readonly humidity: number;
|
|
18
|
+
/** Dry mass fractions over gas species, summing to one. */
|
|
19
|
+
readonly composition: Readonly<Record<string, number>>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Dry air at 20 °C and one atmosphere, at 50% relative humidity.
|
|
23
|
+
*
|
|
24
|
+
* **Argon is lumped into nitrogen**, per the species table's own note: it is 0.93% of air and
|
|
25
|
+
* chemically inert, so all it contributes is heat capacity and dilution. What that costs is
|
|
26
|
+
* measurable and small — the mean molar mass comes out at 28.86 g/mol against air's real 28.96,
|
|
27
|
+
* which is 0.4% — and what it buys is not carrying a sixteenth element for a gas that never reacts.
|
|
28
|
+
*
|
|
29
|
+
* The mass fractions are chosen so the *volume* fraction of oxygen comes back at 20.95%, because
|
|
30
|
+
* that is the number every extinction criterion in `§12` is written against.
|
|
31
|
+
*/
|
|
32
|
+
export declare const STANDARD_AIR: AmbientState;
|
|
33
|
+
/** The gas species a field tracks unless a consumer names its own. */
|
|
34
|
+
export declare const STANDARD_FIELD_SPECIES: readonly string[];
|
|
35
|
+
/**
|
|
36
|
+
* Saturation vapour pressure of water, Pa, by the Magnus form.
|
|
37
|
+
*
|
|
38
|
+
* Authoring-time: it calls `exp`, and `§7` keeps transcendentals off the tick. A field fills a new
|
|
39
|
+
* cell from the ambient, which is a discrete event rather than a per-tick one, and CH-5's
|
|
40
|
+
* evaporation will need this tabulated the way every rate already is.
|
|
41
|
+
*/
|
|
42
|
+
export declare function saturationVapourPressure(temperature: number): number;
|
|
43
|
+
/**
|
|
44
|
+
* Mass of each tracked species in one cell of ambient air, kg.
|
|
45
|
+
*
|
|
46
|
+
* From the ideal gas law at the ambient temperature and pressure, with the humidity turned into
|
|
47
|
+
* water vapour by its partial pressure. Writes into `out`, one entry per field species.
|
|
48
|
+
*/
|
|
49
|
+
export declare function ambientCellMass(registry: SpeciesRegistry, fieldSpecies: Int32Array, ambient: AmbientState, volume: number, out: Float64Array): void;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** Pa. */
|
|
2
|
+
export const STANDARD_PRESSURE_PA = 101325;
|
|
3
|
+
/** J/(mol·K). */
|
|
4
|
+
export const GAS_CONSTANT = 8.314462618;
|
|
5
|
+
/**
|
|
6
|
+
* Dry air at 20 °C and one atmosphere, at 50% relative humidity.
|
|
7
|
+
*
|
|
8
|
+
* **Argon is lumped into nitrogen**, per the species table's own note: it is 0.93% of air and
|
|
9
|
+
* chemically inert, so all it contributes is heat capacity and dilution. What that costs is
|
|
10
|
+
* measurable and small — the mean molar mass comes out at 28.86 g/mol against air's real 28.96,
|
|
11
|
+
* which is 0.4% — and what it buys is not carrying a sixteenth element for a gas that never reacts.
|
|
12
|
+
*
|
|
13
|
+
* The mass fractions are chosen so the *volume* fraction of oxygen comes back at 20.95%, because
|
|
14
|
+
* that is the number every extinction criterion in `§12` is written against.
|
|
15
|
+
*/
|
|
16
|
+
export const STANDARD_AIR = {
|
|
17
|
+
temperature: 293.15,
|
|
18
|
+
pressure: STANDARD_PRESSURE_PA,
|
|
19
|
+
humidity: 0.5,
|
|
20
|
+
composition: { N2: 0.767074, O2: 0.232285, CO2: 0.000641 },
|
|
21
|
+
};
|
|
22
|
+
/** The gas species a field tracks unless a consumer names its own. */
|
|
23
|
+
export const STANDARD_FIELD_SPECIES = [
|
|
24
|
+
'O2',
|
|
25
|
+
'N2',
|
|
26
|
+
'CO2',
|
|
27
|
+
'H2O(g)',
|
|
28
|
+
'CO',
|
|
29
|
+
'CH4',
|
|
30
|
+
'H2',
|
|
31
|
+
'C2H4',
|
|
32
|
+
'NH3',
|
|
33
|
+
'HCN',
|
|
34
|
+
'HCl',
|
|
35
|
+
'SO2',
|
|
36
|
+
];
|
|
37
|
+
/**
|
|
38
|
+
* Saturation vapour pressure of water, Pa, by the Magnus form.
|
|
39
|
+
*
|
|
40
|
+
* Authoring-time: it calls `exp`, and `§7` keeps transcendentals off the tick. A field fills a new
|
|
41
|
+
* cell from the ambient, which is a discrete event rather than a per-tick one, and CH-5's
|
|
42
|
+
* evaporation will need this tabulated the way every rate already is.
|
|
43
|
+
*/
|
|
44
|
+
export function saturationVapourPressure(temperature) {
|
|
45
|
+
const celsius = temperature - 273.15;
|
|
46
|
+
// determinism: build-time — a field fills a new cell from the ambient, which is a discrete event
|
|
47
|
+
return 610.94 * Math.exp((17.625 * celsius) / (celsius + 243.04));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Mass of each tracked species in one cell of ambient air, kg.
|
|
51
|
+
*
|
|
52
|
+
* From the ideal gas law at the ambient temperature and pressure, with the humidity turned into
|
|
53
|
+
* water vapour by its partial pressure. Writes into `out`, one entry per field species.
|
|
54
|
+
*/
|
|
55
|
+
export function ambientCellMass(registry, fieldSpecies, ambient, volume, out) {
|
|
56
|
+
const vapourPressure = ambient.humidity * saturationVapourPressure(ambient.temperature);
|
|
57
|
+
const dryPressure = Math.max(0, ambient.pressure - vapourPressure);
|
|
58
|
+
const totalMoles = (ambient.pressure * volume) / (GAS_CONSTANT * ambient.temperature);
|
|
59
|
+
const vapourMoles = (vapourPressure * volume) / (GAS_CONSTANT * ambient.temperature);
|
|
60
|
+
const dryMoles = totalMoles - vapourMoles;
|
|
61
|
+
/* The dry composition is by mass, so its mean molar mass is what converts moles to kilograms. */
|
|
62
|
+
let inverseMean = 0;
|
|
63
|
+
for (const [id, fraction] of Object.entries(ambient.composition)) {
|
|
64
|
+
const species = registry.indexOf(id);
|
|
65
|
+
if (species < 0)
|
|
66
|
+
throw new Error(`ambient names "${id}", which is not a registered species`);
|
|
67
|
+
inverseMean += fraction / registry.molarMass(species);
|
|
68
|
+
}
|
|
69
|
+
const meanMolarMass = 1 / inverseMean;
|
|
70
|
+
const dryMass = dryMoles * meanMolarMass;
|
|
71
|
+
out.fill(0);
|
|
72
|
+
for (let i = 0; i < fieldSpecies.length; i++) {
|
|
73
|
+
const id = registry.idOf(fieldSpecies[i]);
|
|
74
|
+
const fraction = ambient.composition[id];
|
|
75
|
+
if (fraction !== undefined)
|
|
76
|
+
out[i] = fraction * dryMass;
|
|
77
|
+
if (id === 'H2O(g)') {
|
|
78
|
+
out[i] = out[i] + vapourMoles * registry.molarMass(fieldSpecies[i]);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
void dryPressure;
|
|
82
|
+
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import type { SpeciesRegistry } from '../species/registry.ts';
|
|
2
|
+
import type { ReactionRegistry } from '../reaction/registry.ts';
|
|
3
|
+
import { type AmbientState } from './ambient.ts';
|
|
4
|
+
/** Cells along one edge of a chunk. */
|
|
5
|
+
export declare const CHUNK_CELLS = 8;
|
|
6
|
+
/** Metres along one edge of a chunk, at the default cell size. */
|
|
7
|
+
export declare const CHUNK_SIZE = 8;
|
|
8
|
+
export interface AtmosphereOptions {
|
|
9
|
+
/** Metres per cell. One by default, which makes a chunk eight metres across. */
|
|
10
|
+
readonly cellSize?: number;
|
|
11
|
+
readonly ambient?: AmbientState;
|
|
12
|
+
/** Gas species to track. Twelve by default; a consumer may name fewer and pay less. */
|
|
13
|
+
readonly species?: readonly string[];
|
|
14
|
+
/**
|
|
15
|
+
* How many chunks the live set may grow to before its edge becomes far field.
|
|
16
|
+
*
|
|
17
|
+
* A plume growing the live set is correct — smoke really does reach new ground — and unbounded
|
|
18
|
+
* growth is not. Past the cap a boundary face vents to the ambient instead of creating a
|
|
19
|
+
* neighbour, which degrades a plume into a dissipating one rather than failing.
|
|
20
|
+
*/
|
|
21
|
+
readonly maxChunks?: number;
|
|
22
|
+
/** How fast an open boundary equalises with the far field, per second. */
|
|
23
|
+
readonly openness?: number;
|
|
24
|
+
/** Diffusivity between neighbouring cells, m²/s. */
|
|
25
|
+
readonly diffusivity?: number;
|
|
26
|
+
/** Where `reactionIds` are looked up. */
|
|
27
|
+
readonly reactions?: ReactionRegistry;
|
|
28
|
+
/**
|
|
29
|
+
* Gas-phase reactions to run in every cell, by id.
|
|
30
|
+
*
|
|
31
|
+
* `§8.4`'s combustion, and it is **not a second implementation** of the solver a shell uses — it
|
|
32
|
+
* is that one, given a cell. Which is what makes a flame stand off a surface rather than sit on
|
|
33
|
+
* it: burning is something the *air* does, in the place where the mixture is right.
|
|
34
|
+
*/
|
|
35
|
+
readonly reactionIds?: readonly string[];
|
|
36
|
+
}
|
|
37
|
+
export declare class AtmosphereField {
|
|
38
|
+
private readonly registry;
|
|
39
|
+
readonly cellSize: number;
|
|
40
|
+
readonly cellVolume: number;
|
|
41
|
+
readonly ambient: AmbientState;
|
|
42
|
+
/** Global species indices this field tracks, in storage order. */
|
|
43
|
+
readonly species: Int32Array;
|
|
44
|
+
/** kg per species per cell, then one enthalpy per cell, then soot and aerosol. */
|
|
45
|
+
private readonly chunks;
|
|
46
|
+
private readonly blocked;
|
|
47
|
+
private readonly coords;
|
|
48
|
+
private readonly byKey;
|
|
49
|
+
/** Each chunk's packed `key(cx, cy, cz)`, parallel to `chunks`. */
|
|
50
|
+
private readonly keys;
|
|
51
|
+
/**
|
|
52
|
+
* Chunk indices in **key order**, which is the order every sweep and every reduction walks.
|
|
53
|
+
*
|
|
54
|
+
* **`chunks` is in creation order and creation order is history.** Two runs of one world that
|
|
55
|
+
* realise their chunks in a different sequence — a consumer spawning two logs the other way round
|
|
56
|
+
* is enough — hold identical per-cell state in differently-ordered arrays, and every `+=` over
|
|
57
|
+
* them then lands in a different order. Floating-point addition is not associative, so the answer
|
|
58
|
+
* differs in the last bit, and `fingerprintChemistry` reports a divergence that did not happen.
|
|
59
|
+
*
|
|
60
|
+
* Worse than the report: the sweep accumulates into shared `deltas`, so the *state* diverges too
|
|
61
|
+
* and a stiff exponential rate law grows the difference. Measured before this existed: two logs
|
|
62
|
+
* eight metres apart, spawned in the other order, left **1,202 cells** differing after sixty
|
|
63
|
+
* ticks.
|
|
64
|
+
*
|
|
65
|
+
* `key` is a pure function of the chunk's coordinates, so this order is a fact about geometry and
|
|
66
|
+
* nothing else. Maintained by insertion at creation, which is rare; the sweeps pay one indirection
|
|
67
|
+
* per chunk, not per cell.
|
|
68
|
+
*/
|
|
69
|
+
private readonly order;
|
|
70
|
+
/** Scratch for one sweep round, so the walk does not observe chunks created during it. */
|
|
71
|
+
private round;
|
|
72
|
+
/** Which sweep last visited each chunk, so a chunk created mid-sweep is swept exactly once. */
|
|
73
|
+
private readonly sweptAt;
|
|
74
|
+
private pass;
|
|
75
|
+
private readonly stride;
|
|
76
|
+
private readonly enthalpySlot;
|
|
77
|
+
private readonly sootSlot;
|
|
78
|
+
private readonly aerosolSlot;
|
|
79
|
+
/** Where `C(soot)` sits among the tracked species, or -1 where this field does not carry it. */
|
|
80
|
+
private readonly sootSpecies;
|
|
81
|
+
/** Slots of every carried species in the liquid phase: droplets, which is what an aerosol is. */
|
|
82
|
+
private readonly aerosolSpecies;
|
|
83
|
+
/** One cell of untouched air, filled once and copied into every new cell. */
|
|
84
|
+
readonly maxChunks: number;
|
|
85
|
+
private readonly openness;
|
|
86
|
+
private readonly diffusivity;
|
|
87
|
+
/** Deltas for one step, so no cell sees a neighbour a previous cell already changed. */
|
|
88
|
+
private readonly deltas;
|
|
89
|
+
/** What has left through an open boundary: per species, then enthalpy, soot and aerosol. */
|
|
90
|
+
private readonly vented;
|
|
91
|
+
private ventedEnthalpyTotal;
|
|
92
|
+
private readonly ambientCell;
|
|
93
|
+
private readonly ambientEnthalpy;
|
|
94
|
+
private readonly ambientMass;
|
|
95
|
+
/**
|
|
96
|
+
* The ambient temperature **as `cellTemperature` computes it**, not as it was authored.
|
|
97
|
+
*
|
|
98
|
+
* The same arithmetic over the same doubles in the same order, so a cell that has been realised
|
|
99
|
+
* and one that has not report the identical number. See `temperatureAt`.
|
|
100
|
+
*/
|
|
101
|
+
private readonly ambientTemperatureExact;
|
|
102
|
+
/** `saturationVapourPressure` sampled once, so `exp` never runs on a query. */
|
|
103
|
+
private readonly saturationTable;
|
|
104
|
+
private readonly network;
|
|
105
|
+
private readonly combustible;
|
|
106
|
+
private readonly molarMass;
|
|
107
|
+
/** J/(kg·K) at the standard state, one per tracked species. Resolved once. */
|
|
108
|
+
private readonly heatCapacity;
|
|
109
|
+
private readonly localOf;
|
|
110
|
+
constructor(registry: SpeciesRegistry, options?: AtmosphereOptions);
|
|
111
|
+
get chunkCount(): number;
|
|
112
|
+
/** How many species this field tracks. */
|
|
113
|
+
get speciesCount(): number;
|
|
114
|
+
/** The local slot a global species occupies, or -1. */
|
|
115
|
+
slotOf(species: number): number;
|
|
116
|
+
private key;
|
|
117
|
+
/** The chunk holding a point, creating it if `create`. `-1` where none exists. */
|
|
118
|
+
private chunkAt;
|
|
119
|
+
private createChunk;
|
|
120
|
+
/** Index of the cell containing a point within its chunk. */
|
|
121
|
+
private cellIn;
|
|
122
|
+
/** The base offset of the cell containing a point, or `-1` where no chunk exists. */
|
|
123
|
+
private baseAt;
|
|
124
|
+
private dataAt;
|
|
125
|
+
/** Add (or with a negative value, remove) mass of one species at a point. */
|
|
126
|
+
addSpecies(x: number, y: number, z: number, species: number, kilograms: number): void;
|
|
127
|
+
addHeat(x: number, y: number, z: number, joules: number): void;
|
|
128
|
+
addSoot(x: number, y: number, z: number, kilograms: number): void;
|
|
129
|
+
addAerosol(x: number, y: number, z: number, kilograms: number): void;
|
|
130
|
+
/** Mark a cell as occupied by geometry. A blocked face transports nothing. */
|
|
131
|
+
setBlocked(x: number, y: number, z: number, blocked: boolean): void;
|
|
132
|
+
isBlocked(x: number, y: number, z: number): boolean;
|
|
133
|
+
speciesMassAt(x: number, y: number, z: number, species: number): number;
|
|
134
|
+
/**
|
|
135
|
+
* Kilograms of soot in the cell: the `C(soot)` its own chemistry made, plus anything added.
|
|
136
|
+
*
|
|
137
|
+
* **Two sources on purpose.** Soot a fire produced is the `C(soot)` *species*, which keeps its
|
|
138
|
+
* carbon inside `elementTotals` — a channel carrying mass outside the ledger would make the most
|
|
139
|
+
* valuable assertion in the track quietly wrong every time something smoked. The channel is for a
|
|
140
|
+
* consumer who wants smoke without chemistry behind it: a smoke grenade, a chimney, a dust cloud.
|
|
141
|
+
*/
|
|
142
|
+
sootAt(x: number, y: number, z: number): number;
|
|
143
|
+
/**
|
|
144
|
+
* Kilograms of suspended droplets in the cell — the pale half of smoke.
|
|
145
|
+
*
|
|
146
|
+
* Soot is near-black and a droplet is near-white, so the *ratio* between them is what colours a
|
|
147
|
+
* plume: `§17` reads it, and a wet fire steams white where a rich one smokes black with neither
|
|
148
|
+
* being chosen. Symmetric with `sootAt`, and for the same two reasons.
|
|
149
|
+
*
|
|
150
|
+
* **Two sources, exactly as soot has.** Anything the chemistry condensed is a *species* — wood tar
|
|
151
|
+
* coming out of a cooling plume is the case this was built for — so its mass stays inside
|
|
152
|
+
* `elementTotals` and the most valuable assertion in the track keeps covering it. The channel
|
|
153
|
+
* `addAerosol` writes is for a consumer who wants steam without a boiler behind it: a kettle
|
|
154
|
+
* dressed for a scene, a waterfall's spray, a breath on a cold morning.
|
|
155
|
+
*/
|
|
156
|
+
aerosolAt(x: number, y: number, z: number): number;
|
|
157
|
+
/**
|
|
158
|
+
* The upward speed this cell's own lightness gives it, m/s.
|
|
159
|
+
*
|
|
160
|
+
* **The field's own number rather than a second one.** `§17` gives a smoke particle its velocity
|
|
161
|
+
* from "the cell's advected plus buoyant velocity", and this is the buoyant half — the same
|
|
162
|
+
* function transport uses, so a particle rises at the speed the gas carrying it actually rises at
|
|
163
|
+
* rather than at a rate chosen to look right.
|
|
164
|
+
*/
|
|
165
|
+
riseAt(x: number, y: number, z: number): number;
|
|
166
|
+
/** Total moles of gas in the cell containing a point. */
|
|
167
|
+
molesAt(x: number, y: number, z: number): number;
|
|
168
|
+
/** Joules held by the cell containing a point. */
|
|
169
|
+
enthalpyAt(x: number, y: number, z: number): number;
|
|
170
|
+
/** kg of gas in the cell containing a point. */
|
|
171
|
+
massAt(x: number, y: number, z: number): number;
|
|
172
|
+
/**
|
|
173
|
+
* K, from the cell's enthalpy and the heat capacity of what is in it.
|
|
174
|
+
*
|
|
175
|
+
* A gas mixture has no phase plateau, so this is a division rather than the curve inversion a
|
|
176
|
+
* parcel needs — and it is evaluated at the standard state's heat capacity, which is where the
|
|
177
|
+
* mixture's `cp` is anchored.
|
|
178
|
+
*/
|
|
179
|
+
/**
|
|
180
|
+
* K of the cell containing a point.
|
|
181
|
+
*
|
|
182
|
+
* **`ambientTemperatureExact` rather than `this.ambient.temperature`, and the difference is a
|
|
183
|
+
* determinism bug rather than a rounding nicety.** A cell nobody has touched is not stored; one a
|
|
184
|
+
* neighbour realised is. Returning the *authored* 293.15 for the first and the *derived*
|
|
185
|
+
* 293.15000000000003 for the second made a cell's reported temperature depend on whether some
|
|
186
|
+
* other parcel had happened to create the chunk first — which put an order dependence into
|
|
187
|
+
* convection, and from there into whether a log ignites on tick N or tick N+1.
|
|
188
|
+
*
|
|
189
|
+
* Found by `world/fingerprint.test.ts`: two logs spawned the other way round diverged in the
|
|
190
|
+
* eleventh digit on their very first tick, and nothing about them was different.
|
|
191
|
+
*/
|
|
192
|
+
temperatureAt(x: number, y: number, z: number): number;
|
|
193
|
+
private cellTemperature;
|
|
194
|
+
/** Volume fraction of one species, which for a gas is its mole fraction. */
|
|
195
|
+
volumeFractionAt(x: number, y: number, z: number, species: number): number;
|
|
196
|
+
/**
|
|
197
|
+
* Volume fraction of the cell that is gas which can burn.
|
|
198
|
+
*
|
|
199
|
+
* What a flammability limit is checked against. Below the lower limit there is not enough fuel to
|
|
200
|
+
* carry a flame; above the upper there is not enough air, which is why a fuel-soaked rag smokes
|
|
201
|
+
* rather than flames until it thins out.
|
|
202
|
+
*/
|
|
203
|
+
fuelFractionAt(x: number, y: number, z: number): number;
|
|
204
|
+
oxygenFractionAt(x: number, y: number, z: number): number;
|
|
205
|
+
/** Parts per million by volume, which is the reading a game acts on. */
|
|
206
|
+
concentrationAt(x: number, y: number, z: number, species: number): number;
|
|
207
|
+
/** kg/m³. */
|
|
208
|
+
densityAt(x: number, y: number, z: number): number;
|
|
209
|
+
/** Pa, by the ideal gas law over what the cell holds at the temperature it is. */
|
|
210
|
+
pressureAt(x: number, y: number, z: number): number;
|
|
211
|
+
/**
|
|
212
|
+
* How thick the smoke is, as an extinction coefficient in inverse metres.
|
|
213
|
+
*
|
|
214
|
+
* **The quantity `visibilityAt` is derived from**, and the one a consumer attenuating a light
|
|
215
|
+
* along a ray actually needs: metres of visibility is one answer to it and `exp(−σ·d)` is another.
|
|
216
|
+
* Exporting only the first would have every consumer inverting Koschmieder to get back here.
|
|
217
|
+
*/
|
|
218
|
+
smokeDensityAt(x: number, y: number, z: number): number;
|
|
219
|
+
/**
|
|
220
|
+
* How far you can see, metres.
|
|
221
|
+
*
|
|
222
|
+
* Koschmieder over the soot's extinction. Clamped at ten kilometres, past which the answer stops
|
|
223
|
+
* being about the smoke.
|
|
224
|
+
*/
|
|
225
|
+
visibilityAt(x: number, y: number, z: number): number;
|
|
226
|
+
/**
|
|
227
|
+
* Relative humidity, 0 to 1: the water vapour present against what this air could hold.
|
|
228
|
+
*
|
|
229
|
+
* **The reading that decides whether anything dries.** `§8.1`'s evaporation is driven by the
|
|
230
|
+
* vapour pressure *deficit*, so washing dries in wind and nothing dries in fog — and the same
|
|
231
|
+
* number is why a fire dries a room out while removing no water at all: warm air could hold more,
|
|
232
|
+
* so the same vapour is a smaller fraction of saturation.
|
|
233
|
+
*
|
|
234
|
+
* **Saturation is tabulated**, per `§7`'s rule, because it is `exp` of a rational function of
|
|
235
|
+
* temperature and this is a query a script may make every frame. 256 knots over 200-500 K, where
|
|
236
|
+
* linear interpolation is under a tenth of a percent.
|
|
237
|
+
*/
|
|
238
|
+
humidityAt(x: number, y: number, z: number): number;
|
|
239
|
+
/** Pa, off the table `SATURATION_KNOTS` describes. Clamped at both ends rather than extrapolated. */
|
|
240
|
+
private saturation;
|
|
241
|
+
/** Kilograms of one species above what the ambient would have held. Diagnostics and tests. */
|
|
242
|
+
totalAdded(species: number): number;
|
|
243
|
+
/**
|
|
244
|
+
* Advance the field one step: expansion, buoyancy, wind, diffusion and the far field.
|
|
245
|
+
*
|
|
246
|
+
* **Every transfer is flux-form and conservative** — computed once for a face and applied to both
|
|
247
|
+
* sides — which is a departure from `§9` worth recording. The design chose semi-Lagrangian
|
|
248
|
+
* advection for unconditional stability; semi-Lagrangian is **not conservative**, and
|
|
249
|
+
* conservation is the invariant this whole track is asserted on. Upwind flux has a CFL limit
|
|
250
|
+
* instead, and a CFL limit is handled here the same way stiffness is handled in the reaction
|
|
251
|
+
* solver: by sub-stepping. That is the trade taken deliberately.
|
|
252
|
+
*
|
|
253
|
+
* **Deltas are accumulated and applied afterwards**, so no cell sees a neighbour that an earlier
|
|
254
|
+
* cell in the sweep already changed. A Gauss-Seidel sweep would give an answer that depended on
|
|
255
|
+
* chunk allocation order, which is a determinism hole with no symptom until two runs allocate
|
|
256
|
+
* differently.
|
|
257
|
+
*
|
|
258
|
+
* `windX` and `windZ` come from the one `WindField`, sampled once per frame by the caller.
|
|
259
|
+
*/
|
|
260
|
+
step(dt: number, windX?: number, windZ?: number): void;
|
|
261
|
+
/**
|
|
262
|
+
* Run the gas-phase network in every live cell.
|
|
263
|
+
*
|
|
264
|
+
* After transport rather than before, so a cell burns the mixture it actually ended the step
|
|
265
|
+
* with — which is what makes a flame follow the fuel rather than lag a step behind it.
|
|
266
|
+
*/
|
|
267
|
+
private react;
|
|
268
|
+
/** The upward speed a cell's own lightness gives it, m/s. */
|
|
269
|
+
private buoyantSpeed;
|
|
270
|
+
/** Resolve a world cell to a packed chunk-and-cell, or -1. */
|
|
271
|
+
private resolve;
|
|
272
|
+
private sweep;
|
|
273
|
+
/** One chunk's faces, for `sweep`. Every transfer lands in `deltas` and is applied afterwards. */
|
|
274
|
+
private sweepChunk;
|
|
275
|
+
/**
|
|
276
|
+
* Whether this cell holds anything the far field would not.
|
|
277
|
+
*
|
|
278
|
+
* What bounds the live set. A cell indistinguishable from the outdoors has nothing to spread, so
|
|
279
|
+
* it never claims a neighbour — which is why a world with one campfire holds the chunks around
|
|
280
|
+
* that fire rather than a column of air to the sky.
|
|
281
|
+
*/
|
|
282
|
+
private departsFromAmbient;
|
|
283
|
+
/** One internal face: bulk transfer by wind, buoyancy and pressure, plus diffusion. */
|
|
284
|
+
private face;
|
|
285
|
+
/** A face onto the far field: relax toward ambient and book what crossed. */
|
|
286
|
+
private ventFace;
|
|
287
|
+
/** Moles of each element the field holds, over every live chunk. Writes into `out`. */
|
|
288
|
+
elementTotals(out: Float64Array): void;
|
|
289
|
+
/** Total gas mass across every live chunk, kg. */
|
|
290
|
+
totalMass(): number;
|
|
291
|
+
/** Kilograms of one species that have left through an open boundary. */
|
|
292
|
+
ventedMass(species: number): number;
|
|
293
|
+
/** Joules that have left through an open boundary. */
|
|
294
|
+
ventedEnthalpy(): number;
|
|
295
|
+
/** Kilograms that have crossed the far-field boundary in total, in either direction. */
|
|
296
|
+
ventedTotalMass(): number;
|
|
297
|
+
/**
|
|
298
|
+
* Moles of each element that have crossed the far-field boundary. Writes into `out`.
|
|
299
|
+
*
|
|
300
|
+
* Live chunks alone do not balance, and the reason is not transport: **creating a chunk draws a
|
|
301
|
+
* chunk of air in from outside**, which is matter appearing in the live set. The ledger records
|
|
302
|
+
* it, so `elementTotals + ventedElementTotals` is invariant under chunk creation as well as under
|
|
303
|
+
* everything else — and a conservation assertion over a growing plume is possible at all.
|
|
304
|
+
*/
|
|
305
|
+
ventedElementTotals(out: Float64Array): void;
|
|
306
|
+
/** Total enthalpy across every live chunk, J. */
|
|
307
|
+
totalEnthalpy(): number;
|
|
308
|
+
}
|