@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.
Files changed (142) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +194 -0
  4. package/dist/conservation.d.ts +9 -0
  5. package/dist/conservation.js +38 -0
  6. package/dist/element/elements.d.ts +31 -0
  7. package/dist/element/elements.js +73 -0
  8. package/dist/element/matrix.d.ts +27 -0
  9. package/dist/element/matrix.js +80 -0
  10. package/dist/field/ambient.d.ts +49 -0
  11. package/dist/field/ambient.js +82 -0
  12. package/dist/field/atmosphere.d.ts +308 -0
  13. package/dist/field/atmosphere.js +1033 -0
  14. package/dist/ignition/criteria.d.ts +97 -0
  15. package/dist/ignition/criteria.js +135 -0
  16. package/dist/ignition/events.d.ts +56 -0
  17. package/dist/ignition/events.js +90 -0
  18. package/dist/index.d.ts +84 -0
  19. package/dist/index.js +66 -0
  20. package/dist/library/biological.d.ts +6 -0
  21. package/dist/library/biological.js +229 -0
  22. package/dist/library/common.d.ts +26 -0
  23. package/dist/library/common.js +208 -0
  24. package/dist/library/food.d.ts +9 -0
  25. package/dist/library/food.js +272 -0
  26. package/dist/library/fuel.d.ts +19 -0
  27. package/dist/library/fuel.js +290 -0
  28. package/dist/library/library.d.ts +45 -0
  29. package/dist/library/library.js +29 -0
  30. package/dist/library/metal.d.ts +6 -0
  31. package/dist/library/metal.js +181 -0
  32. package/dist/library/mineral.d.ts +4 -0
  33. package/dist/library/mineral.js +177 -0
  34. package/dist/library/organic.d.ts +12 -0
  35. package/dist/library/organic.js +489 -0
  36. package/dist/library/polymer.d.ts +4 -0
  37. package/dist/library/polymer.js +237 -0
  38. package/dist/parcel/conduction.d.ts +20 -0
  39. package/dist/parcel/conduction.js +160 -0
  40. package/dist/parcel/shells.d.ts +49 -0
  41. package/dist/parcel/shells.js +73 -0
  42. package/dist/parcel/store.d.ts +395 -0
  43. package/dist/parcel/store.js +1152 -0
  44. package/dist/parcel/tier.d.ts +48 -0
  45. package/dist/parcel/tier.js +51 -0
  46. package/dist/present/audio.d.ts +41 -0
  47. package/dist/present/audio.js +50 -0
  48. package/dist/present/blackbody.d.ts +55 -0
  49. package/dist/present/blackbody.js +112 -0
  50. package/dist/present/flame.d.ts +34 -0
  51. package/dist/present/flame.js +103 -0
  52. package/dist/present/index.d.ts +25 -0
  53. package/dist/present/index.js +22 -0
  54. package/dist/present/install.d.ts +87 -0
  55. package/dist/present/install.js +157 -0
  56. package/dist/present/smoke.d.ts +63 -0
  57. package/dist/present/smoke.js +85 -0
  58. package/dist/present/surface.d.ts +40 -0
  59. package/dist/present/surface.js +76 -0
  60. package/dist/reaction/localise.d.ts +16 -0
  61. package/dist/reaction/localise.js +95 -0
  62. package/dist/reaction/rates.d.ts +59 -0
  63. package/dist/reaction/rates.js +101 -0
  64. package/dist/reaction/reaction.d.ts +178 -0
  65. package/dist/reaction/reaction.js +104 -0
  66. package/dist/reaction/registry.d.ts +47 -0
  67. package/dist/reaction/registry.js +215 -0
  68. package/dist/reaction/solver.d.ts +51 -0
  69. package/dist/reaction/solver.js +277 -0
  70. package/dist/species/registry.d.ts +26 -0
  71. package/dist/species/registry.js +97 -0
  72. package/dist/species/species.d.ts +48 -0
  73. package/dist/species/species.js +20 -0
  74. package/dist/species/standard.d.ts +30 -0
  75. package/dist/species/standard.js +436 -0
  76. package/dist/species/thermal.d.ts +18 -0
  77. package/dist/species/thermal.js +72 -0
  78. package/dist/substance/boiling.d.ts +29 -0
  79. package/dist/substance/boiling.js +54 -0
  80. package/dist/substance/composition.d.ts +34 -0
  81. package/dist/substance/composition.js +53 -0
  82. package/dist/substance/registry.d.ts +86 -0
  83. package/dist/substance/registry.js +187 -0
  84. package/dist/substance/substance.d.ts +94 -0
  85. package/dist/substance/substance.js +48 -0
  86. package/dist/testing/drift.d.ts +15 -0
  87. package/dist/testing/drift.js +29 -0
  88. package/dist/transport/contacts.d.ts +22 -0
  89. package/dist/transport/contacts.js +50 -0
  90. package/dist/transport/radiation.d.ts +101 -0
  91. package/dist/transport/radiation.js +199 -0
  92. package/dist/transport/world.d.ts +92 -0
  93. package/dist/transport/world.js +381 -0
  94. package/dist/world/fingerprint.d.ts +31 -0
  95. package/dist/world/fingerprint.js +70 -0
  96. package/package.json +100 -0
  97. package/src/conservation.ts +44 -0
  98. package/src/element/elements.ts +80 -0
  99. package/src/element/matrix.ts +89 -0
  100. package/src/field/ambient.ts +111 -0
  101. package/src/field/atmosphere.ts +1159 -0
  102. package/src/ignition/criteria.ts +177 -0
  103. package/src/ignition/events.ts +100 -0
  104. package/src/index.ts +213 -0
  105. package/src/library/biological.ts +252 -0
  106. package/src/library/common.ts +224 -0
  107. package/src/library/food.ts +302 -0
  108. package/src/library/fuel.ts +299 -0
  109. package/src/library/library.ts +62 -0
  110. package/src/library/metal.ts +186 -0
  111. package/src/library/mineral.ts +194 -0
  112. package/src/library/organic.ts +545 -0
  113. package/src/library/polymer.ts +243 -0
  114. package/src/parcel/conduction.ts +188 -0
  115. package/src/parcel/shells.ts +100 -0
  116. package/src/parcel/store.ts +1270 -0
  117. package/src/parcel/tier.ts +60 -0
  118. package/src/present/audio.ts +54 -0
  119. package/src/present/blackbody.ts +118 -0
  120. package/src/present/flame.ts +111 -0
  121. package/src/present/index.ts +25 -0
  122. package/src/present/install.ts +209 -0
  123. package/src/present/smoke.ts +139 -0
  124. package/src/present/surface.ts +107 -0
  125. package/src/reaction/localise.ts +126 -0
  126. package/src/reaction/rates.ts +153 -0
  127. package/src/reaction/reaction.ts +235 -0
  128. package/src/reaction/registry.ts +257 -0
  129. package/src/reaction/solver.ts +326 -0
  130. package/src/species/registry.ts +117 -0
  131. package/src/species/species.ts +53 -0
  132. package/src/species/standard.ts +449 -0
  133. package/src/species/thermal.ts +74 -0
  134. package/src/substance/boiling.ts +64 -0
  135. package/src/substance/composition.ts +81 -0
  136. package/src/substance/registry.ts +223 -0
  137. package/src/substance/substance.ts +148 -0
  138. package/src/testing/drift.ts +28 -0
  139. package/src/transport/contacts.ts +56 -0
  140. package/src/transport/radiation.ts +238 -0
  141. package/src/transport/world.ts +437 -0
  142. package/src/world/fingerprint.ts +106 -0
@@ -0,0 +1,53 @@
1
+ /**
2
+ * How far a hand-written composition may be from summing to one.
3
+ *
4
+ * Generous enough for six fractions typed to three decimals, tight enough that a transposed digit
5
+ * is a refusal rather than a material that quietly loses half a percent of its mass on every
6
+ * conservation check.
7
+ */
8
+ export const SUM_TOLERANCE = 1e-6;
9
+ /**
10
+ * A composition from species ids and mass fractions.
11
+ *
12
+ * Fails fast on anything wrong, per `AGENTS.md`: an unknown id, a fraction that is negative or not
13
+ * a number, a sum that is not one. Each names the offending value, because "invalid composition" in
14
+ * a stack trace is a sentence somebody has to reproduce to act on.
15
+ */
16
+ export function massFractions(registry, entries) {
17
+ const ids = Object.keys(entries);
18
+ if (ids.length === 0) {
19
+ throw new Error('a composition may not be empty');
20
+ }
21
+ const pairs = [];
22
+ let sum = 0;
23
+ for (const id of ids) {
24
+ const species = registry.indexOf(id);
25
+ if (species < 0) {
26
+ throw new Error(`composition names "${id}", which is not a registered species`);
27
+ }
28
+ const fraction = entries[id];
29
+ if (!Number.isFinite(fraction) || fraction < 0) {
30
+ throw new Error(`composition gives "${id}" a mass fraction of ${fraction}`);
31
+ }
32
+ sum += fraction;
33
+ pairs.push({ species, fraction });
34
+ }
35
+ if (Math.abs(sum - 1) > SUM_TOLERANCE) {
36
+ throw new Error(`composition sums to ${sum} rather than 1`);
37
+ }
38
+ /*
39
+ * Sorted by species index, not by the order the author typed.
40
+ *
41
+ * Adding doubles is not associative, so two spellings of the same material would reduce to
42
+ * different last bits — and a conservation check is exactly where a last bit becomes a failing
43
+ * assertion nobody can explain. Sorting here makes the storage canonical, once, at init.
44
+ */
45
+ pairs.sort((a, b) => a.species - b.species);
46
+ const species = new Int32Array(pairs.length);
47
+ const fraction = new Float64Array(pairs.length);
48
+ for (let i = 0; i < pairs.length; i++) {
49
+ species[i] = pairs[i].species;
50
+ fraction[i] = pairs[i].fraction;
51
+ }
52
+ return { species, fraction };
53
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * The substances a world knows about, and the enthalpy curve each one resolves through.
3
+ *
4
+ * A curve is expensive to build and constant for the life of a substance, so it is built once here
5
+ * rather than per parcel. Two hundred oak logs share one curve, which is what makes the whole
6
+ * approach affordable: the per-parcel cost of a temperature is a binary search over 256 knots and
7
+ * nothing else.
8
+ *
9
+ * A substance is an `i32` index after registration, the convention CH-0 established for a species
10
+ * and `drift/physics` established for a body.
11
+ */
12
+ import { type ConductionModel } from '../parcel/conduction.ts';
13
+ import type { SpeciesRegistry } from '../species/registry.ts';
14
+ import type { AppearanceModel, SubstanceThermal } from './substance.ts';
15
+ import type { IgnitionModel } from '../ignition/criteria.ts';
16
+ import type { ReactionRegistry } from '../reaction/registry.ts';
17
+ import { type SubstanceReactions } from '../reaction/solver.ts';
18
+ export declare class SubstanceRegistry {
19
+ readonly species: SpeciesRegistry;
20
+ private readonly reactions?;
21
+ private readonly ids;
22
+ private readonly byId;
23
+ private readonly definitions;
24
+ private readonly conduction;
25
+ /**
26
+ * One conduction model per shell count a tier can reduce this substance to, indexed by that count.
27
+ *
28
+ * **Built at registration, because `shellGeometry` calls `Math.cbrt`** and `§15` keeps that off the
29
+ * simulation path entirely — its result is not pinned down by ECMAScript. A level-of-detail tier
30
+ * that computed geometry when it changed would have put it back there. Four shapes and at most
31
+ * sixteen counts is nothing to precompute and the alternative is a determinism hole.
32
+ */
33
+ private readonly byShellCount;
34
+ private readonly reactionModels;
35
+ /** Global species indices this substance's parcels track, ascending. */
36
+ private readonly speciesSets;
37
+ /** Global species index to local slot, or -1. One row per substance. */
38
+ private readonly localSlots;
39
+ private readonly densities;
40
+ private readonly porosities;
41
+ private readonly emissivities;
42
+ private readonly ignitions;
43
+ private readonly appearances;
44
+ constructor(species: SpeciesRegistry, reactions?: ReactionRegistry | undefined);
45
+ get count(): number;
46
+ define(substance: SubstanceThermal): number;
47
+ indexOf(id: string): number;
48
+ idOf(substance: number): string;
49
+ conductionOf(substance: number): ConductionModel;
50
+ /**
51
+ * The conduction model for a substance resolved to `shells`, which a tier may have reduced it to.
52
+ *
53
+ * Precomputed at registration; see `byShellCount` for why it is not derived on demand.
54
+ */
55
+ conductionAt(substance: number, shells: number): ConductionModel;
56
+ densityOf(substance: number): number;
57
+ /** Void fraction, 0..1. Zero means it exchanges no gas with the air at all. */
58
+ porosityOf(substance: number): number;
59
+ /** How readily it radiates and absorbs, 0..1. */
60
+ emissivityOf(substance: number): number;
61
+ /** When this material catches, or `null` where nobody said it does. */
62
+ ignitionOf(substance: number): IgnitionModel | null;
63
+ /**
64
+ * What it looks like, or `null` where nobody said.
65
+ *
66
+ * `null` rather than a grey default: `present/` writes nothing for a substance with no appearance
67
+ * rather than inventing one, so a consumer sees their own material unchanged instead of finding
68
+ * the engine has quietly recoloured it.
69
+ */
70
+ appearanceOf(substance: number): AppearanceModel | null;
71
+ definitionOf(substance: number): SubstanceThermal;
72
+ /** Global species indices this substance's parcels carry, ascending. */
73
+ speciesSetOf(substance: number): Int32Array;
74
+ /** The local slot a global species occupies for this substance, or -1. */
75
+ localSlotOf(substance: number, species: number): number;
76
+ reactionsOf(substance: number): SubstanceReactions;
77
+ /**
78
+ * The species set and the localised, packed reaction network.
79
+ *
80
+ * The set is the composition's species plus everything its reactions touch, so a shell's
81
+ * composition is a dense run of the few species it can hold rather than a row over all
82
+ * sixty-eight. Built here, once, because it is a property of the substance.
83
+ */
84
+ private buildReactions;
85
+ private check;
86
+ }
@@ -0,0 +1,187 @@
1
+ /**
2
+ * The substances a world knows about, and the enthalpy curve each one resolves through.
3
+ *
4
+ * A curve is expensive to build and constant for the life of a substance, so it is built once here
5
+ * rather than per parcel. Two hundred oak logs share one curve, which is what makes the whole
6
+ * approach affordable: the per-parcel cost of a temperature is a binary search over 256 knots and
7
+ * nothing else.
8
+ *
9
+ * A substance is an `i32` index after registration, the convention CH-0 established for a species
10
+ * and `drift/physics` established for a body.
11
+ */
12
+ import {} from '../parcel/conduction.js';
13
+ import { SHAPE_CYLINDER, shellGeometry } from '../parcel/shells.js';
14
+ import { MAX_SUBSTANCE_SPECIES } from '../reaction/solver.js';
15
+ import { localiseReactions } from '../reaction/localise.js';
16
+ export class SubstanceRegistry {
17
+ species;
18
+ reactions;
19
+ ids = [];
20
+ byId = new Map();
21
+ definitions = [];
22
+ conduction = [];
23
+ /**
24
+ * One conduction model per shell count a tier can reduce this substance to, indexed by that count.
25
+ *
26
+ * **Built at registration, because `shellGeometry` calls `Math.cbrt`** and `§15` keeps that off the
27
+ * simulation path entirely — its result is not pinned down by ECMAScript. A level-of-detail tier
28
+ * that computed geometry when it changed would have put it back there. Four shapes and at most
29
+ * sixteen counts is nothing to precompute and the alternative is a determinism hole.
30
+ */
31
+ byShellCount = [];
32
+ reactionModels = [];
33
+ /** Global species indices this substance's parcels track, ascending. */
34
+ speciesSets = [];
35
+ /** Global species index to local slot, or -1. One row per substance. */
36
+ localSlots = [];
37
+ densities = [];
38
+ porosities = [];
39
+ emissivities = [];
40
+ ignitions = [];
41
+ appearances = [];
42
+ constructor(species, reactions) {
43
+ this.species = species;
44
+ this.reactions = reactions;
45
+ }
46
+ get count() {
47
+ return this.ids.length;
48
+ }
49
+ define(substance) {
50
+ if (this.byId.has(substance.id)) {
51
+ throw new Error(`substance "${substance.id}" is already registered`);
52
+ }
53
+ if (!(substance.density > 0)) {
54
+ throw new Error(`substance "${substance.id}" has a density of ${substance.density}`);
55
+ }
56
+ /* The shell geometry is a pure function of a shape and a count, so it is resolved here once and
57
+ multiplied by a parcel's own depth at use — which keeps `Math.cbrt`, whose result ECMAScript
58
+ does not pin down, off the simulation path entirely. */
59
+ const geometry = shellGeometry(substance.shape ?? SHAPE_CYLINDER, substance.shells ?? 4);
60
+ const { set, local, model } = this.buildReactions(substance);
61
+ const index = this.ids.length;
62
+ this.speciesSets.push(set);
63
+ this.localSlots.push(local);
64
+ this.reactionModels.push(model);
65
+ const conductivity = substance.conductivity ?? null;
66
+ this.conduction.push({ geometry, conductivity });
67
+ const shape = substance.shape ?? SHAPE_CYLINDER;
68
+ const perCount = [null];
69
+ for (let count = 1; count <= geometry.count; count++) {
70
+ perCount.push({ geometry: shellGeometry(shape, count), conductivity });
71
+ }
72
+ this.byShellCount.push(perCount);
73
+ this.ids.push(substance.id);
74
+ this.byId.set(substance.id, index);
75
+ this.definitions.push(substance);
76
+ this.densities.push(substance.density);
77
+ this.porosities.push(substance.porosity ?? 0);
78
+ this.emissivities.push(substance.emissivity ?? 0.9);
79
+ this.ignitions.push(substance.ignition ?? null);
80
+ this.appearances.push(substance.appearance ?? null);
81
+ return index;
82
+ }
83
+ indexOf(id) {
84
+ return this.byId.get(id) ?? -1;
85
+ }
86
+ idOf(substance) {
87
+ return this.ids[this.check(substance)];
88
+ }
89
+ conductionOf(substance) {
90
+ return this.conduction[this.check(substance)];
91
+ }
92
+ /**
93
+ * The conduction model for a substance resolved to `shells`, which a tier may have reduced it to.
94
+ *
95
+ * Precomputed at registration; see `byShellCount` for why it is not derived on demand.
96
+ */
97
+ conductionAt(substance, shells) {
98
+ const row = this.byShellCount[this.check(substance)];
99
+ const model = shells >= 1 && shells < row.length ? row[shells] : null;
100
+ return model ?? this.conduction[substance];
101
+ }
102
+ densityOf(substance) {
103
+ return this.densities[this.check(substance)];
104
+ }
105
+ /** Void fraction, 0..1. Zero means it exchanges no gas with the air at all. */
106
+ porosityOf(substance) {
107
+ return this.porosities[this.check(substance)];
108
+ }
109
+ /** How readily it radiates and absorbs, 0..1. */
110
+ emissivityOf(substance) {
111
+ return this.emissivities[this.check(substance)];
112
+ }
113
+ /** When this material catches, or `null` where nobody said it does. */
114
+ ignitionOf(substance) {
115
+ return this.ignitions[this.check(substance)];
116
+ }
117
+ /**
118
+ * What it looks like, or `null` where nobody said.
119
+ *
120
+ * `null` rather than a grey default: `present/` writes nothing for a substance with no appearance
121
+ * rather than inventing one, so a consumer sees their own material unchanged instead of finding
122
+ * the engine has quietly recoloured it.
123
+ */
124
+ appearanceOf(substance) {
125
+ return this.appearances[this.check(substance)];
126
+ }
127
+ definitionOf(substance) {
128
+ return this.definitions[this.check(substance)];
129
+ }
130
+ /** Global species indices this substance's parcels carry, ascending. */
131
+ speciesSetOf(substance) {
132
+ return this.speciesSets[this.check(substance)];
133
+ }
134
+ /** The local slot a global species occupies for this substance, or -1. */
135
+ localSlotOf(substance, species) {
136
+ const row = this.localSlots[this.check(substance)];
137
+ return species >= 0 && species < row.length ? row[species] : -1;
138
+ }
139
+ reactionsOf(substance) {
140
+ return this.reactionModels[this.check(substance)];
141
+ }
142
+ /**
143
+ * The species set and the localised, packed reaction network.
144
+ *
145
+ * The set is the composition's species plus everything its reactions touch, so a shell's
146
+ * composition is a dense run of the few species it can hold rather than a row over all
147
+ * sixty-eight. Built here, once, because it is a property of the substance.
148
+ */
149
+ buildReactions(substance) {
150
+ const ids = substance.reactions ?? [];
151
+ const present = new Set();
152
+ for (const s of substance.composition.species)
153
+ present.add(s);
154
+ if (this.reactions !== undefined) {
155
+ for (const id of ids) {
156
+ const r = this.reactions.indexOf(id);
157
+ if (r < 0)
158
+ throw new Error(`substance "${substance.id}" declares reaction "${id}", which is not registered`);
159
+ for (const sp of this.reactions.reactantSpecies(r))
160
+ present.add(sp);
161
+ for (const sp of this.reactions.productSpecies(r))
162
+ present.add(sp);
163
+ }
164
+ }
165
+ else if (ids.length > 0) {
166
+ throw new Error(`substance "${substance.id}" declares reactions, and this registry was built with none`);
167
+ }
168
+ const set = Int32Array.from([...present].sort((a, b) => a - b));
169
+ if (set.length > MAX_SUBSTANCE_SPECIES) {
170
+ throw new Error(`substance "${substance.id}" would track ${set.length} species; the limit is ${MAX_SUBSTANCE_SPECIES}`);
171
+ }
172
+ const local = new Int32Array(this.species.count).fill(-1);
173
+ for (let i = 0; i < set.length; i++)
174
+ local[set[i]] = i;
175
+ return {
176
+ set,
177
+ local,
178
+ model: localiseReactions(this.species, this.reactions, ids, set, `substance "${substance.id}"`),
179
+ };
180
+ }
181
+ check(substance) {
182
+ if (!Number.isInteger(substance) || substance < 0 || substance >= this.ids.length) {
183
+ throw new Error(`substance ${substance} is outside a registry of ${this.ids.length}`);
184
+ }
185
+ return substance;
186
+ }
187
+ }
@@ -0,0 +1,94 @@
1
+ import type { SpeciesRegistry } from '../species/registry.ts';
2
+ import { type Composition } from './composition.ts';
3
+ import type { Conductivity } from '../parcel/conduction.ts';
4
+ import type { Shape } from '../parcel/shells.ts';
5
+ import type { IgnitionModel } from '../ignition/criteria.ts';
6
+ export interface SubstanceThermal {
7
+ readonly id: string;
8
+ /**
9
+ * What it is made of **at the bottom of the curve**, as mass fractions summing to one.
10
+ *
11
+ * The rule matters and is not obvious: transitions are applied in ascending temperature order, so
12
+ * a substance is authored as its coldest form. Water is `H2O(s)` with two transitions, not
13
+ * `H2O(l)` with one in each direction. A transition whose source species is absent when it is
14
+ * reached is refused, naming both, rather than silently doing nothing.
15
+ */
16
+ readonly composition: Composition;
17
+ /** kg/m³ of the dry matter. */
18
+ readonly density: number;
19
+ /**
20
+ * W/(m·K), rising with temperature. **Optional, and conduction refuses a substance without one**,
21
+ * naming it — a thermal conductivity nobody stated is a number somebody invented, and it decides
22
+ * whether a log takes twenty seconds or twenty minutes.
23
+ */
24
+ readonly conductivity?: Conductivity;
25
+ /** How many nested shells of equal mass. Four by default; see `§4` for why not one and why not sixteen. */
26
+ readonly shells?: number;
27
+ /** What it is shaped like, which decides how those shells divide. A cylinder by default: a log. */
28
+ readonly shape?: Shape;
29
+ /**
30
+ * Void fraction, 0..1. What decides how freely gas moves between this material and the air.
31
+ *
32
+ * A charcoal briquette breathes and a pane of glass does not, and that is most of the difference
33
+ * between something that smoulders and something that does not. Zero by default, which means a
34
+ * substance exchanges no gas at all until somebody says how open it is.
35
+ */
36
+ readonly porosity?: number;
37
+ /** How readily it radiates and absorbs, 0..1. Most non-metals sit near 0.9, which is the default. */
38
+ readonly emissivity?: number;
39
+ /**
40
+ * Reactions this material can undergo, by id.
41
+ *
42
+ * A substance's parcels track masses for its composition species **plus every species these
43
+ * reactions touch**, so declaring one is what makes room for its products. Wood that pyrolyses
44
+ * holds char and steam it did not start with.
45
+ */
46
+ readonly reactions?: readonly string[];
47
+ /**
48
+ * When this material catches and when it stops.
49
+ *
50
+ * Absent means it never catches at all, which is the right default: a substance nobody described
51
+ * as flammable should not be, and inventing an ignition temperature for glass is worse than
52
+ * declining to have one.
53
+ */
54
+ readonly ignition?: IgnitionModel;
55
+ /**
56
+ * What it looks like, and every field is a measurable property of the material rather than a
57
+ * choice about how to draw it.
58
+ *
59
+ * Absent means a consumer supplies their own colours, which is the right default: a substance
60
+ * nobody described the appearance of should not have one invented for it. `present/` refuses to
61
+ * write an albedo it was never given rather than writing grey.
62
+ */
63
+ readonly appearance?: AppearanceModel;
64
+ }
65
+ /**
66
+ * `§17`'s three numbers: what a material looks like before it burns, after it burns, and how dirty
67
+ * its smoke is on the way.
68
+ *
69
+ * **Soot yield belongs on the substance and `§8.4` says why**: polystyrene sooting heavily and
70
+ * methanol burning invisibly are the two ends of a scale, and which end a fuel sits at is a property
71
+ * of the fuel. It is the one number that decides whether a fire is seen at all in daylight.
72
+ */
73
+ export interface AppearanceModel {
74
+ /** Linear RGB before anything happens to it. */
75
+ readonly albedo: readonly [number, number, number];
76
+ /** Linear RGB when it is entirely char. Near-black for anything that chars at all. */
77
+ readonly charAlbedo: readonly [number, number, number];
78
+ /** kg of soot per kg of volatiles burnt. Wood is about 0.015; polystyrene is 0.16. */
79
+ readonly sootYield: number;
80
+ }
81
+ /**
82
+ * A composition with dry-basis moisture folded into it.
83
+ *
84
+ * **"12% moisture content" means 12 kg of water per 100 kg of *dry* matter**, so water is
85
+ * `m / (1 + m)` of the whole and every dry component is scaled by `1 / (1 + m)`. Treating the
86
+ * figure as a wet-basis fraction is wrong by 1.4 points at 12% and by 33 points for green wood at
87
+ * 60%, which is exactly the range where it decides whether a log catches fire at all.
88
+ *
89
+ * Water the composition already held is added to rather than replaced, because a substance can be
90
+ * authored wet: a stew is mostly water before anybody adds moisture to it.
91
+ */
92
+ export declare function wetComposition(registry: SpeciesRegistry, dry: Composition, moisture: number): Composition;
93
+ /** Moles of each element in one kilogram of a composition. Writes into `out`, allocates nothing. */
94
+ export declare function compositionElements(registry: SpeciesRegistry, composition: Composition, out: Float64Array): void;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * What a material is, thermally: a composition, a density, and the transitions it passes through.
3
+ *
4
+ * CH-1's half of `Substance`. The design's `§5` describes the whole of it — conductivity,
5
+ * emissivity, porosity, shells, reactions, ignition, appearance — and each field arrives with the
6
+ * phase that reads it. Declaring them all here with nothing reading them would be the speculative
7
+ * abstraction `AGENTS.md` §Module rules forbids.
8
+ */
9
+ import { ELEMENT_COUNT } from '../element/elements.js';
10
+ import { massFractions } from './composition.js';
11
+ /**
12
+ * A composition with dry-basis moisture folded into it.
13
+ *
14
+ * **"12% moisture content" means 12 kg of water per 100 kg of *dry* matter**, so water is
15
+ * `m / (1 + m)` of the whole and every dry component is scaled by `1 / (1 + m)`. Treating the
16
+ * figure as a wet-basis fraction is wrong by 1.4 points at 12% and by 33 points for green wood at
17
+ * 60%, which is exactly the range where it decides whether a log catches fire at all.
18
+ *
19
+ * Water the composition already held is added to rather than replaced, because a substance can be
20
+ * authored wet: a stew is mostly water before anybody adds moisture to it.
21
+ */
22
+ export function wetComposition(registry, dry, moisture) {
23
+ if (!Number.isFinite(moisture) || moisture < 0) {
24
+ throw new Error(`moisture content is ${moisture}, which is not a non-negative number`);
25
+ }
26
+ if (moisture === 0)
27
+ return dry;
28
+ const scale = 1 / (1 + moisture);
29
+ const entries = {};
30
+ for (let i = 0; i < dry.species.length; i++) {
31
+ entries[registry.idOf(dry.species[i])] = dry.fraction[i] * scale;
32
+ }
33
+ const water = 'H2O(l)';
34
+ entries[water] = (entries[water] ?? 0) + moisture * scale;
35
+ return massFractions(registry, entries);
36
+ }
37
+ /** Moles of each element in one kilogram of a composition. Writes into `out`, allocates nothing. */
38
+ export function compositionElements(registry, composition, out) {
39
+ if (out.length !== ELEMENT_COUNT) {
40
+ throw new Error(`a totals vector is ${ELEMENT_COUNT} wide, not ${out.length}`);
41
+ }
42
+ out.fill(0);
43
+ for (let i = 0; i < composition.species.length; i++) {
44
+ const species = composition.species[i];
45
+ const kilograms = composition.fraction[i];
46
+ registry.elements.addScaledRow(species, kilograms / registry.molarMass(species), out);
47
+ }
48
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * How far two element vectors have drifted apart, for the conservation harness.
3
+ *
4
+ * Relative rather than absolute, because the elements in a burning world span many orders of
5
+ * magnitude at once: a scene may hold a kilomole of nitrogen and a micromole of sulphur, and an
6
+ * absolute tolerance either passes on a broken sulphur balance or fails on a correct nitrogen one.
7
+ *
8
+ * **With one exception that matters.** Where both sides are effectively zero, a relative measure
9
+ * has nothing to divide by — and an element that appeared from nowhere would then report as no
10
+ * drift at all, which is the single failure this harness exists to catch. So the denominator has a
11
+ * floor of one, making the measure absolute in that regime.
12
+ *
13
+ * Test-side rather than runtime: nothing in the simulation calls it.
14
+ */
15
+ export declare function maxElementDrift(a: Float64Array, b: Float64Array): number;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * How far two element vectors have drifted apart, for the conservation harness.
3
+ *
4
+ * Relative rather than absolute, because the elements in a burning world span many orders of
5
+ * magnitude at once: a scene may hold a kilomole of nitrogen and a micromole of sulphur, and an
6
+ * absolute tolerance either passes on a broken sulphur balance or fails on a correct nitrogen one.
7
+ *
8
+ * **With one exception that matters.** Where both sides are effectively zero, a relative measure
9
+ * has nothing to divide by — and an element that appeared from nowhere would then report as no
10
+ * drift at all, which is the single failure this harness exists to catch. So the denominator has a
11
+ * floor of one, making the measure absolute in that regime.
12
+ *
13
+ * Test-side rather than runtime: nothing in the simulation calls it.
14
+ */
15
+ export function maxElementDrift(a, b) {
16
+ if (a.length !== b.length) {
17
+ throw new Error(`element vectors are ${a.length} and ${b.length} wide`);
18
+ }
19
+ let worst = 0;
20
+ for (let i = 0; i < a.length; i++) {
21
+ const left = a[i];
22
+ const right = b[i];
23
+ const scale = Math.max(1, Math.abs(left), Math.abs(right));
24
+ const drift = Math.abs(left - right) / scale;
25
+ if (drift > worst)
26
+ worst = drift;
27
+ }
28
+ return worst;
29
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Which parcels are touching, this tick.
3
+ *
4
+ * Refilled by the caller each tick rather than held, the same shape `RadiativeSources` takes and
5
+ * for the same reason: contact is a property of the world's arrangement, which this package does
6
+ * not know. In practice it is fed from Track B's contact event buffer, which already reports pairs
7
+ * by feature id — and feeding it is the whole of what a consumer does to make a pan heat a steak.
8
+ */
9
+ export declare class ContactSet {
10
+ private a;
11
+ private b;
12
+ private area;
13
+ private used;
14
+ get count(): number;
15
+ clear(): void;
16
+ /** @param area m² of shared surface, which is what sets how fast heat crosses. */
17
+ add(a: number, b: number, area: number): void;
18
+ firstOf(contact: number): number;
19
+ secondOf(contact: number): number;
20
+ areaOf(contact: number): number;
21
+ private grow;
22
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Which parcels are touching, this tick.
3
+ *
4
+ * Refilled by the caller each tick rather than held, the same shape `RadiativeSources` takes and
5
+ * for the same reason: contact is a property of the world's arrangement, which this package does
6
+ * not know. In practice it is fed from Track B's contact event buffer, which already reports pairs
7
+ * by feature id — and feeding it is the whole of what a consumer does to make a pan heat a steak.
8
+ */
9
+ export class ContactSet {
10
+ a = new Int32Array(32);
11
+ b = new Int32Array(32);
12
+ area = new Float64Array(32);
13
+ used = 0;
14
+ get count() {
15
+ return this.used;
16
+ }
17
+ clear() {
18
+ this.used = 0;
19
+ }
20
+ /** @param area m² of shared surface, which is what sets how fast heat crosses. */
21
+ add(a, b, area) {
22
+ if (this.used === this.a.length)
23
+ this.grow();
24
+ const at = this.used++;
25
+ this.a[at] = a;
26
+ this.b[at] = b;
27
+ this.area[at] = area;
28
+ }
29
+ firstOf(contact) {
30
+ return this.a[contact];
31
+ }
32
+ secondOf(contact) {
33
+ return this.b[contact];
34
+ }
35
+ areaOf(contact) {
36
+ return this.area[contact];
37
+ }
38
+ grow() {
39
+ const size = this.a.length * 2;
40
+ const a = new Int32Array(size);
41
+ a.set(this.a);
42
+ this.a = a;
43
+ const b = new Int32Array(size);
44
+ b.set(this.b);
45
+ this.b = b;
46
+ const area = new Float64Array(size);
47
+ area.set(this.area);
48
+ this.area = area;
49
+ }
50
+ }