@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,302 @@
1
+ /**
2
+ * `@driftengine/chemistry/library/food` — water, fat, protein, starch, sugar, and cooking.
3
+ *
4
+ * **Nothing here is a cooking rule.** Every claim a cook makes about searing is a consequence of one
5
+ * fact the model already had: a wet surface is pinned at the boiling point by CH-6's vaporisation
6
+ * plateau, and Maillard needs forty degrees more than that. So browning is not forbidden while the
7
+ * surface is wet — it is *unreachable*, which is a much stronger statement and costs nothing.
8
+ *
9
+ * The one species this library adds is molten fat, because fat melting is a phase change between two
10
+ * states of one material and this model expresses a phase change as a reaction between two species.
11
+ */
12
+ import { PHASE_LIQUID, type SpeciesDefinition } from '../species/species.ts';
13
+ import {
14
+ KIND_BROWNING,
15
+ KIND_DENATURATION,
16
+ type Reaction,
17
+ phaseChange,
18
+ } from '../reaction/reaction.ts';
19
+ import { SHAPE_SLAB, SHAPE_SPHERE } from '../parcel/shells.ts';
20
+ import { massFractions } from '../substance/composition.ts';
21
+ import { wetComposition } from '../substance/substance.ts';
22
+ import type { SubstanceThermal } from '../substance/substance.ts';
23
+ import type { SubstanceLibrary } from './library.ts';
24
+ import { BOIL_WATER, COMMON_REACTIONS, MELT_ICE } from './common.ts';
25
+
26
+ export const MELT_FAT = 'melt-fat';
27
+ export const DENATURE_MYOSIN = 'denature-myosin';
28
+ export const DENATURE_ALBUMEN = 'denature-albumen';
29
+ export const GELATINISE_COLLAGEN = 'gelatinise-collagen';
30
+ export const HYDROLYSE_STARCH = 'hydrolyse-starch';
31
+ export const BROWN_MAILLARD = 'brown-maillard';
32
+ export const CARAMELISE = 'caramelise';
33
+
34
+ /**
35
+ * Molten fat: the same molecule as `triglyceride(saturated)`, one heat of fusion above it.
36
+ *
37
+ * 190 J/g, which is tristearin's measured enthalpy of fusion, and it is **stored** rather than
38
+ * derived — unavoidably, because a phase pair is two species and the gap between their formation
39
+ * enthalpies *is* the latent heat. Water's two transitions are derived only because CH-0 happened to
40
+ * have published formation enthalpies for ice, water and steam separately; nobody has published one
41
+ * for molten tristearin.
42
+ */
43
+ const MOLTEN_FAT: SpeciesDefinition = {
44
+ id: 'fat(liquid)',
45
+ formula: { C: 57, H: 110, O: 6 },
46
+ phase: PHASE_LIQUID,
47
+ formationEnthalpy: -2024313,
48
+ cpA: 2100,
49
+ pseudo: true,
50
+ };
51
+
52
+ /** K over which butter softens. `§8.1`: a mixture of triglycerides, so a band and not a point. */
53
+ const FAT_BAND = 3;
54
+
55
+ const REACTIONS: readonly Reaction[] = [
56
+ ...COMMON_REACTIONS,
57
+
58
+ /* 33-35 °C, over three kelvin, which is what makes fat render instead of switching state. */
59
+ phaseChange(MELT_FAT, 'triglyceride(saturated)', 'fat(liquid)', 306.15, FAT_BAND),
60
+
61
+ {
62
+ /* Myosin, at 50-55 °C: the meat firms and turns opaque. */
63
+ id: DENATURE_MYOSIN,
64
+ reactants: [{ species: 'protein', moles: 1 }],
65
+ products: [{ species: 'protein(denatured)', moles: 1 }],
66
+ kinetics: { type: 'arrhenius', A: 0.4, activationEnergy: 0 },
67
+ kind: KIND_DENATURATION,
68
+ gates: { minTemperature: 323.15, width: 5 },
69
+ },
70
+ {
71
+ /* Egg white sets at 62 °C, and the yolk at 68 — which is why a 63 °C egg is a thing. The two
72
+ bands are two reactions rather than one, because they are two proteins. */
73
+ id: DENATURE_ALBUMEN,
74
+ reactants: [{ species: 'protein', moles: 1 }],
75
+ products: [{ species: 'protein(denatured)', moles: 1 }],
76
+ kinetics: { type: 'arrhenius', A: 0.4, activationEnergy: 0 },
77
+ kind: KIND_DENATURATION,
78
+ gates: { minTemperature: 335.15, width: 3 },
79
+ },
80
+ {
81
+ /*
82
+ * **The rate is back-derived from two measured times**, which is what an activation energy is:
83
+ * many hours at 60 °C and about an hour at 80 °C is a factor of eight over twenty kelvin, and
84
+ * that factor fixes `Ea` at 101.6 kJ/mol. `A` then sets the absolute clock from the second time.
85
+ *
86
+ * The consequence is the whole of slow cooking: this reaction and the ones that dry the meat out
87
+ * are a genuine competition between two rates, and at 80 °C this one is winning.
88
+ */
89
+ id: GELATINISE_COLLAGEN,
90
+ reactants: [
91
+ { species: 'collagen', moles: 1 },
92
+ { species: 'H2O(l)', moles: 1 },
93
+ ],
94
+ products: [{ species: 'gelatin', moles: 1 }],
95
+ kinetics: { type: 'arrhenius', A: 3e11, activationEnergy: 101600 },
96
+ kind: KIND_DENATURATION,
97
+ gates: { minTemperature: 333.15, width: 5 },
98
+ },
99
+ {
100
+ /* Starch to sugar, needing water and warmth. It is what puts a reducing sugar on the surface of
101
+ a potato, and therefore what lets a potato brown at all. */
102
+ id: HYDROLYSE_STARCH,
103
+ reactants: [
104
+ { species: 'starch', moles: 1 },
105
+ { species: 'H2O(l)', moles: 1 },
106
+ ],
107
+ products: [{ species: 'glucose', moles: 1 }],
108
+ kinetics: { type: 'arrhenius', A: 1e11, activationEnergy: 100000 },
109
+ kind: KIND_DENATURATION,
110
+ gates: { minTemperature: 328.15, width: 10 },
111
+ },
112
+ {
113
+ /*
114
+ * Browning, at 140 °C, and the product is carbon because carbon is what a crust is.
115
+ *
116
+ * **The amino donor is not a reactant, and that is a stated simplification.** Maillard needs a
117
+ * reducing sugar and an amino group; the sugar is the scarce one and the protein is always there
118
+ * in the foods where this matters, so making the protein a reactant would have bought nothing
119
+ * and cost a nitrogen path with nowhere to go. What is modelled is the sugar being consumed and
120
+ * a dark surface appearing, which is what `§17` reads.
121
+ */
122
+ id: BROWN_MAILLARD,
123
+ reactants: [{ species: 'glucose', moles: 1 }],
124
+ products: [
125
+ { species: 'C(char)', moles: 6 },
126
+ { species: 'H2O(g)', moles: 6 },
127
+ ],
128
+ kinetics: { type: 'arrhenius', A: 4e12, activationEnergy: 130000 },
129
+ kind: KIND_BROWNING,
130
+ gates: { minTemperature: 413.15, width: 8 },
131
+ },
132
+ {
133
+ /* Sugar alone, twenty degrees higher, and no protein needed. */
134
+ id: CARAMELISE,
135
+ reactants: [{ species: 'sucrose', moles: 1 }],
136
+ products: [
137
+ { species: 'C(char)', moles: 12 },
138
+ { species: 'H2O(g)', moles: 11 },
139
+ ],
140
+ kinetics: { type: 'arrhenius', A: 4e12, activationEnergy: 130000 },
141
+ kind: KIND_BROWNING,
142
+ gates: { minTemperature: 433.15, width: 10 },
143
+ },
144
+ ];
145
+
146
+ export const FOOD: SubstanceLibrary = {
147
+ id: 'food',
148
+ species: [MOLTEN_FAT],
149
+ reactions: REACTIONS,
150
+ substances: (species): readonly SubstanceThermal[] => [
151
+ {
152
+ id: 'water',
153
+ composition: massFractions(species, { 'H2O(l)': 1 }),
154
+ density: 997,
155
+ conductivity: { k0: 0.6, k1: 0 },
156
+ shells: 2,
157
+ shape: SHAPE_SPHERE,
158
+ emissivity: 0.96,
159
+ reactions: [BOIL_WATER],
160
+ appearance: {
161
+ albedo: [0.02, 0.03, 0.04],
162
+ charAlbedo: [0.02, 0.03, 0.04],
163
+ sootYield: 0,
164
+ },
165
+ },
166
+ {
167
+ /* Authored as the coldest form, per `SubstanceThermal`: ice with two transitions rather than
168
+ water with one in each direction, because a reaction runs one way. */
169
+ id: 'ice',
170
+ composition: massFractions(species, { 'H2O(s)': 1 }),
171
+ density: 917,
172
+ conductivity: { k0: 2.2, k1: -0.01 },
173
+ shells: 3,
174
+ shape: SHAPE_SPHERE,
175
+ emissivity: 0.97,
176
+ reactions: [MELT_ICE, BOIL_WATER],
177
+ appearance: {
178
+ albedo: [0.72, 0.78, 0.82],
179
+ charAlbedo: [0.72, 0.78, 0.82],
180
+ sootYield: 0,
181
+ },
182
+ },
183
+ {
184
+ /* Eighty percent fat, sixteen water, and the water is why it spits. */
185
+ id: 'butter',
186
+ composition: wetComposition(
187
+ species,
188
+ massFractions(species, {
189
+ 'triglyceride(saturated)': 0.94,
190
+ protein: 0.04,
191
+ ash: 0.02,
192
+ }),
193
+ 0.19,
194
+ ),
195
+ density: 911,
196
+ conductivity: { k0: 0.2, k1: 0 },
197
+ shells: 2,
198
+ shape: SHAPE_SLAB,
199
+ emissivity: 0.93,
200
+ reactions: [MELT_FAT, BOIL_WATER, DENATURE_MYOSIN],
201
+ appearance: {
202
+ albedo: [0.86, 0.76, 0.42],
203
+ charAlbedo: [0.1, 0.06, 0.03],
204
+ sootYield: 0.05,
205
+ },
206
+ },
207
+ {
208
+ /* Seventy-two percent water by mass, which is `moisture: 2.571` on a dry basis — and getting
209
+ that conversion wrong is the mistake `wetComposition` exists to prevent. */
210
+ id: 'beef-muscle',
211
+ composition: wetComposition(
212
+ species,
213
+ massFractions(species, {
214
+ protein: 0.74,
215
+ 'triglyceride(saturated)': 0.15,
216
+ collagen: 0.08,
217
+ glucose: 0.01,
218
+ ash: 0.02,
219
+ }),
220
+ 2.571,
221
+ ),
222
+ density: 1050,
223
+ conductivity: { k0: 0.45, k1: 0.0005 },
224
+ shells: 4,
225
+ shape: SHAPE_SLAB,
226
+ porosity: 0.1,
227
+ emissivity: 0.95,
228
+ reactions: [BOIL_WATER, MELT_FAT, DENATURE_MYOSIN, GELATINISE_COLLAGEN, BROWN_MAILLARD],
229
+ appearance: {
230
+ albedo: [0.42, 0.12, 0.11],
231
+ charAlbedo: [0.05, 0.03, 0.02],
232
+ sootYield: 0.04,
233
+ },
234
+ },
235
+ {
236
+ /* Starch, and the free sugar that lets it brown. A potato browns because starch hydrolyses
237
+ first — which is why a blanched chip browns and a raw one goes grey. */
238
+ id: 'potato',
239
+ composition: wetComposition(
240
+ species,
241
+ massFractions(species, {
242
+ starch: 0.8,
243
+ protein: 0.09,
244
+ glucose: 0.05,
245
+ ash: 0.06,
246
+ }),
247
+ 3.762,
248
+ ),
249
+ density: 1080,
250
+ conductivity: { k0: 0.55, k1: 0.0005 },
251
+ shells: 4,
252
+ shape: SHAPE_SPHERE,
253
+ porosity: 0.15,
254
+ emissivity: 0.93,
255
+ reactions: [BOIL_WATER, HYDROLYSE_STARCH, DENATURE_MYOSIN, BROWN_MAILLARD],
256
+ appearance: {
257
+ albedo: [0.72, 0.62, 0.38],
258
+ charAlbedo: [0.06, 0.04, 0.02],
259
+ sootYield: 0.02,
260
+ },
261
+ },
262
+ {
263
+ /* Eighty-eight percent water, and it sets twelve degrees above where muscle firms. */
264
+ id: 'egg-white',
265
+ composition: wetComposition(
266
+ species,
267
+ massFractions(species, {
268
+ protein: 0.92,
269
+ glucose: 0.02,
270
+ ash: 0.06,
271
+ }),
272
+ 7.333,
273
+ ),
274
+ density: 1040,
275
+ conductivity: { k0: 0.6, k1: 0.0005 },
276
+ shells: 3,
277
+ shape: SHAPE_SPHERE,
278
+ emissivity: 0.95,
279
+ reactions: [BOIL_WATER, DENATURE_ALBUMEN, BROWN_MAILLARD],
280
+ appearance: {
281
+ albedo: [0.88, 0.87, 0.82],
282
+ charAlbedo: [0.08, 0.06, 0.04],
283
+ sootYield: 0.02,
284
+ },
285
+ },
286
+ {
287
+ id: 'sugar',
288
+ composition: massFractions(species, { sucrose: 1 }),
289
+ density: 1590,
290
+ conductivity: { k0: 0.15, k1: 0 },
291
+ shells: 2,
292
+ shape: SHAPE_SLAB,
293
+ emissivity: 0.9,
294
+ reactions: [CARAMELISE],
295
+ appearance: {
296
+ albedo: [0.92, 0.9, 0.86],
297
+ charAlbedo: [0.08, 0.04, 0.02],
298
+ sootYield: 0.03,
299
+ },
300
+ },
301
+ ],
302
+ };
@@ -0,0 +1,299 @@
1
+ /**
2
+ * `@driftengine/chemistry/library/fuel` — the liquids and the gas, and what they do before they burn.
3
+ *
4
+ * **A liquid fuel does not burn; its vapour does.** That is why a pool fire's burning rate is a
5
+ * heat-transfer number rather than a chemical one, why a cold pool of diesel will not light from a
6
+ * match and a cold pool of petrol will, and why throwing sand on one works. So every liquid here
7
+ * carries a vaporisation before it carries a combustion, and the two are ordinary gated reactions.
8
+ *
9
+ * **Six species, and each is an existing liquid plus a measured enthalpy of vaporisation.** Nothing
10
+ * about combustion went into any of them, so the heating values that come out of the burn reactions
11
+ * are predictions — and `fuel.test.ts` checks each against its published figure.
12
+ */
13
+ import { PHASE_GAS, PHASE_LIQUID, type SpeciesDefinition } from '../species/species.ts';
14
+ import { KIND_COMBUSTION, type Reaction, phaseChange } from '../reaction/reaction.ts';
15
+ import { SHAPE_CYLINDER, SHAPE_SLAB } from '../parcel/shells.ts';
16
+ import { massFractions } from '../substance/composition.ts';
17
+ import type { SubstanceThermal } from '../substance/substance.ts';
18
+ import type { SubstanceLibrary } from './library.ts';
19
+ import { COMMON_REACTIONS, GAS_REACTIONS } from './common.ts';
20
+
21
+ export const BURN_ETHANOL = 'burn-ethanol';
22
+ export const BURN_PETROL = 'burn-petrol';
23
+ export const BURN_DIESEL = 'burn-diesel';
24
+ export const BURN_KEROSENE = 'burn-kerosene';
25
+ export const BURN_PROPANE = 'burn-propane';
26
+ export const BURN_WAX = 'burn-wax';
27
+
28
+ /** What an `AtmosphereField` is given where any of these fuels is in play. */
29
+ export const FUEL_GAS_REACTIONS: readonly string[] = [
30
+ ...GAS_REACTIONS,
31
+ BURN_ETHANOL,
32
+ BURN_PETROL,
33
+ BURN_DIESEL,
34
+ BURN_KEROSENE,
35
+ BURN_PROPANE,
36
+ BURN_WAX,
37
+ ];
38
+
39
+ /*
40
+ * The vapours. Each formation enthalpy is the liquid's plus that liquid's measured enthalpy of
41
+ * vaporisation at 298.15 K, and three of the four with a published gas-phase value land within
42
+ * 0.6% of it — ethanol −235,300 against −234,800, octane −208,600 against −208,750, dodecane
43
+ * −289,400 against −290,900. That agreement is the arithmetic checking itself.
44
+ */
45
+ const VAPOURS: readonly SpeciesDefinition[] = [
46
+ {
47
+ id: 'ethanol(g)',
48
+ formula: { C: 2, H: 6, O: 1 },
49
+ phase: PHASE_GAS,
50
+ formationEnthalpy: -235300,
51
+ cpA: 1420,
52
+ cpB: 3.5,
53
+ },
54
+ {
55
+ id: 'octane(g)',
56
+ formula: { C: 8, H: 18 },
57
+ phase: PHASE_GAS,
58
+ formationEnthalpy: -208600,
59
+ cpA: 1650,
60
+ cpB: 4.0,
61
+ },
62
+ {
63
+ id: 'diesel(g)',
64
+ formula: { C: 12, H: 26 },
65
+ phase: PHASE_GAS,
66
+ formationEnthalpy: -289400,
67
+ cpA: 1640,
68
+ cpB: 4.0,
69
+ pseudo: true,
70
+ },
71
+ {
72
+ id: 'kerosene(g)',
73
+ formula: { C: 12, H: 23 },
74
+ phase: PHASE_GAS,
75
+ formationEnthalpy: -219166,
76
+ cpA: 1600,
77
+ cpB: 3.8,
78
+ pseudo: true,
79
+ },
80
+ /*
81
+ * Wax gets two, because a candle is two plateaus in series. Fusion is 200 J/g and vaporisation is
82
+ * 284 J/g, both measured on paraffin; over 352.69 g/mol those are 70,538 and 100,000 J/mol.
83
+ */
84
+ {
85
+ id: 'wax(l)',
86
+ formula: { C: 25, H: 52 },
87
+ phase: PHASE_LIQUID,
88
+ formationEnthalpy: -798661,
89
+ cpA: 2300,
90
+ pseudo: true,
91
+ },
92
+ {
93
+ id: 'wax(g)',
94
+ formula: { C: 25, H: 52 },
95
+ phase: PHASE_GAS,
96
+ formationEnthalpy: -698661,
97
+ cpA: 2400,
98
+ cpB: 3.0,
99
+ pseudo: true,
100
+ },
101
+ ];
102
+
103
+ const REACTIONS: readonly Reaction[] = [
104
+ ...COMMON_REACTIONS,
105
+
106
+ /*
107
+ * The boiling points, and the **widths are the interesting half**: ethanol is one molecule and
108
+ * boils at a point, where diesel and kerosene are distillation cuts and boil over tens of kelvin.
109
+ * Same gate, different width, and the consequence is that a cut keeps giving off vapour across a
110
+ * range of surface temperatures instead of all at once.
111
+ */
112
+ phaseChange('vaporise-ethanol', 'ethanol', 'ethanol(g)', 351.45, 0.5),
113
+ phaseChange('vaporise-petrol', 'octane', 'octane(g)', 398.75, 1),
114
+ phaseChange('vaporise-diesel', 'diesel', 'diesel(g)', 489.15, 20),
115
+ phaseChange('vaporise-kerosene', 'kerosene', 'kerosene(g)', 470.15, 20),
116
+ phaseChange('melt-wax', 'wax', 'wax(l)', 330.15, 8),
117
+ phaseChange('vaporise-wax', 'wax(l)', 'wax(g)', 660.15, 10),
118
+
119
+ {
120
+ id: BURN_ETHANOL,
121
+ reactants: [
122
+ { species: 'ethanol(g)', moles: 1 },
123
+ { species: 'O2', moles: 3 },
124
+ ],
125
+ products: [
126
+ { species: 'CO2', moles: 2 },
127
+ { species: 'H2O(g)', moles: 3 },
128
+ ],
129
+ kinetics: { type: 'arrhenius', A: 1e12, activationEnergy: 125500 },
130
+ kind: KIND_COMBUSTION,
131
+ },
132
+ {
133
+ id: BURN_PETROL,
134
+ reactants: [
135
+ { species: 'octane(g)', moles: 1 },
136
+ { species: 'O2', moles: 12.5 },
137
+ ],
138
+ products: [
139
+ { species: 'CO2', moles: 8 },
140
+ { species: 'H2O(g)', moles: 9 },
141
+ ],
142
+ kinetics: { type: 'arrhenius', A: 4e11, activationEnergy: 125500 },
143
+ kind: KIND_COMBUSTION,
144
+ },
145
+ {
146
+ id: BURN_DIESEL,
147
+ reactants: [
148
+ { species: 'diesel(g)', moles: 1 },
149
+ { species: 'O2', moles: 18.5 },
150
+ ],
151
+ products: [
152
+ { species: 'CO2', moles: 12 },
153
+ { species: 'H2O(g)', moles: 13 },
154
+ ],
155
+ kinetics: { type: 'arrhenius', A: 3e11, activationEnergy: 125500 },
156
+ kind: KIND_COMBUSTION,
157
+ },
158
+ {
159
+ id: BURN_KEROSENE,
160
+ reactants: [
161
+ { species: 'kerosene(g)', moles: 1 },
162
+ { species: 'O2', moles: 17.75 },
163
+ ],
164
+ products: [
165
+ { species: 'CO2', moles: 12 },
166
+ { species: 'H2O(g)', moles: 11.5 },
167
+ ],
168
+ kinetics: { type: 'arrhenius', A: 3e11, activationEnergy: 125500 },
169
+ kind: KIND_COMBUSTION,
170
+ },
171
+ {
172
+ /* Already a gas, so it has no vaporisation and no flash point — which is exactly why a leak is
173
+ dangerous at any temperature and a diesel spill is not. */
174
+ id: BURN_PROPANE,
175
+ reactants: [
176
+ { species: 'propane', moles: 1 },
177
+ { species: 'O2', moles: 5 },
178
+ ],
179
+ products: [
180
+ { species: 'CO2', moles: 3 },
181
+ { species: 'H2O(g)', moles: 4 },
182
+ ],
183
+ kinetics: { type: 'arrhenius', A: 8e11, activationEnergy: 125500 },
184
+ kind: KIND_COMBUSTION,
185
+ },
186
+ {
187
+ id: BURN_WAX,
188
+ reactants: [
189
+ { species: 'wax(g)', moles: 1 },
190
+ { species: 'O2', moles: 38 },
191
+ ],
192
+ products: [
193
+ { species: 'CO2', moles: 25 },
194
+ { species: 'H2O(g)', moles: 26 },
195
+ ],
196
+ kinetics: { type: 'arrhenius', A: 2e11, activationEnergy: 125500 },
197
+ kind: KIND_COMBUSTION,
198
+ },
199
+ ];
200
+
201
+ /**
202
+ * A liquid fuel's `pilotedSurfaceK` is its **flash point** and `autoSurfaceK` its autoignition
203
+ * temperature, and the gap between them is enormous where it matters.
204
+ *
205
+ * Petrol flashes at −43 °C and autoignites at 280; diesel flashes at 52 °C and autoignites at 210.
206
+ * That is the whole difference between the two in a garage: petrol is above its flash point on the
207
+ * coldest day of the year and diesel is below it on the hottest, so one needs a spark it will
208
+ * certainly find and the other needs a spark it may not.
209
+ */
210
+ export const FUEL: SubstanceLibrary = {
211
+ id: 'fuel',
212
+ species: VAPOURS,
213
+ reactions: REACTIONS,
214
+ substances: (species): readonly SubstanceThermal[] => [
215
+ {
216
+ id: 'ethanol',
217
+ composition: massFractions(species, { ethanol: 1 }),
218
+ density: 789,
219
+ conductivity: { k0: 0.17, k1: 0 },
220
+ shells: 2,
221
+ shape: SHAPE_SLAB,
222
+ emissivity: 0.95,
223
+ reactions: ['vaporise-ethanol'],
224
+ /* And its flame is nearly invisible, because there is no soot in it at all — one of the two
225
+ ends of `§8.4`'s sooting scale, and a genuine hazard in a real spirit fire. */
226
+ ignition: { pilotedSurfaceK: 286, autoSurfaceK: 636, criticalMassFlux: 0.001 },
227
+ appearance: {
228
+ albedo: [0.03, 0.04, 0.05],
229
+ charAlbedo: [0.03, 0.04, 0.05],
230
+ sootYield: 0.008,
231
+ },
232
+ },
233
+ {
234
+ id: 'petrol',
235
+ composition: massFractions(species, { octane: 1 }),
236
+ density: 745,
237
+ conductivity: { k0: 0.14, k1: 0 },
238
+ shells: 2,
239
+ shape: SHAPE_SLAB,
240
+ emissivity: 0.95,
241
+ reactions: ['vaporise-petrol'],
242
+ ignition: { pilotedSurfaceK: 230, autoSurfaceK: 553, criticalMassFlux: 0.001 },
243
+ appearance: {
244
+ albedo: [0.3, 0.26, 0.1],
245
+ charAlbedo: [0.3, 0.26, 0.1],
246
+ sootYield: 0.1,
247
+ },
248
+ },
249
+ {
250
+ id: 'diesel',
251
+ composition: massFractions(species, { diesel: 1 }),
252
+ density: 832,
253
+ conductivity: { k0: 0.13, k1: 0 },
254
+ shells: 2,
255
+ shape: SHAPE_SLAB,
256
+ emissivity: 0.95,
257
+ reactions: ['vaporise-diesel'],
258
+ ignition: { pilotedSurfaceK: 325, autoSurfaceK: 483, criticalMassFlux: 0.0012 },
259
+ appearance: {
260
+ albedo: [0.2, 0.17, 0.1],
261
+ charAlbedo: [0.2, 0.17, 0.1],
262
+ sootYield: 0.13,
263
+ },
264
+ },
265
+ {
266
+ id: 'kerosene',
267
+ composition: massFractions(species, { kerosene: 1 }),
268
+ density: 810,
269
+ conductivity: { k0: 0.13, k1: 0 },
270
+ shells: 2,
271
+ shape: SHAPE_SLAB,
272
+ emissivity: 0.95,
273
+ reactions: ['vaporise-kerosene'],
274
+ ignition: { pilotedSurfaceK: 311, autoSurfaceK: 493, criticalMassFlux: 0.0012 },
275
+ appearance: {
276
+ albedo: [0.24, 0.22, 0.16],
277
+ charAlbedo: [0.24, 0.22, 0.16],
278
+ sootYield: 0.12,
279
+ },
280
+ },
281
+ {
282
+ /* Solid, and it has to melt and then boil before anything burns. That is a candle. */
283
+ id: 'paraffin-wax',
284
+ composition: massFractions(species, { wax: 1 }),
285
+ density: 900,
286
+ conductivity: { k0: 0.25, k1: 0 },
287
+ shells: 3,
288
+ shape: SHAPE_CYLINDER,
289
+ emissivity: 0.93,
290
+ reactions: ['melt-wax', 'vaporise-wax'],
291
+ ignition: { pilotedSurfaceK: 472, autoSurfaceK: 573, criticalMassFlux: 0.002 },
292
+ appearance: {
293
+ albedo: [0.86, 0.85, 0.8],
294
+ charAlbedo: [0.1, 0.09, 0.08],
295
+ sootYield: 0.06,
296
+ },
297
+ },
298
+ ],
299
+ };
@@ -0,0 +1,62 @@
1
+ /**
2
+ * What a substance family is, and how one is installed.
3
+ *
4
+ * **`§2` of the design applies the payload rule to *data*, which this engine has not had to do
5
+ * before.** A substance is about forty numbers, a composition and a reaction list; forty of them is
6
+ * a real payload and most games use ten. So each family is its own entry point, and a consumer who
7
+ * defines three substances of their own imports none of these and pays for none of them.
8
+ *
9
+ * **Substances are a function of the registry rather than constants.** A composition is mass
10
+ * fractions over species *indices*, and there are no indices until something is registered — so the
11
+ * only honest shape is a function called once, at init, with the registry that will hold them.
12
+ *
13
+ * **A library may register species of its own**, and two of them do. That is better than growing
14
+ * `species/standard.ts`: `tar` is meaningless outside a wood fire and molten wax outside a candle,
15
+ * so putting either in the standard table would charge every consumer in the engine for both.
16
+ */
17
+ import type { SpeciesDefinition } from '../species/species.ts';
18
+ import type { SpeciesRegistry } from '../species/registry.ts';
19
+ import type { Reaction } from '../reaction/reaction.ts';
20
+ import type { ReactionRegistry } from '../reaction/registry.ts';
21
+ import type { SubstanceRegistry } from '../substance/registry.ts';
22
+ import type { SubstanceThermal } from '../substance/substance.ts';
23
+
24
+ export interface SubstanceLibrary {
25
+ readonly id: string;
26
+ /** Species this family needs that the standard table does not carry. Usually none. */
27
+ readonly species: readonly SpeciesDefinition[];
28
+ readonly reactions: readonly Reaction[];
29
+ substances(species: SpeciesRegistry): readonly SubstanceThermal[];
30
+ }
31
+
32
+ /**
33
+ * Species, then reactions, then substances — **skipping whatever is already registered**.
34
+ *
35
+ * The skipping is the whole reason this is not three loops of `register`. Gas-phase combustion
36
+ * belongs to every family that burns, and registering an id twice throws by design, so a consumer
37
+ * installing `organic` and `fuel` together would otherwise fail at init on the first reaction they
38
+ * share. Skipping also makes installing a family twice a no-op rather than a crash, which is what a
39
+ * consumer assembling a world out of several libraries will do by accident sooner or later.
40
+ *
41
+ * **It does not check that the existing registration matches.** Two libraries declaring different
42
+ * reactions under one id would silently take the first, and nothing here would say so — but every
43
+ * id in these libraries is defined in exactly one place, and a consumer who shadows one has said
44
+ * something deliberate. Refusing it would mean comparing stoichiometry and kinetics for equality,
45
+ * which is a real cost at init for a case the libraries themselves cannot produce.
46
+ */
47
+ export function installLibrary(
48
+ library: SubstanceLibrary,
49
+ species: SpeciesRegistry,
50
+ reactions: ReactionRegistry,
51
+ substances: SubstanceRegistry,
52
+ ): void {
53
+ for (const definition of library.species) {
54
+ if (species.indexOf(definition.id) < 0) species.register(definition);
55
+ }
56
+ for (const reaction of library.reactions) {
57
+ if (reactions.indexOf(reaction.id) < 0) reactions.register(reaction);
58
+ }
59
+ for (const substance of library.substances(species)) {
60
+ if (substances.indexOf(substance.id) < 0) substances.define(substance);
61
+ }
62
+ }