@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,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@driftengine/chemistry/library/polymer` — six plastics, and what each one does in a fire.
|
|
3
|
+
*
|
|
4
|
+
* **The interesting numbers here are the two that are not enthalpies.** PVC's limiting oxygen index
|
|
5
|
+
* is 0.45, more than twice what air provides, so it burns only while something else holds a flame to
|
|
6
|
+
* it — which is the whole reason it goes in walls. And PVC's chlorine and polyurethane's nitrogen
|
|
7
|
+
* come out as HCl and HCN because their formulas say so, which is why a burning sofa is the
|
|
8
|
+
* dangerous one and why nothing here had to be told that.
|
|
9
|
+
*
|
|
10
|
+
* **The per-kilogram gasification endotherms run high**, and `library.ts`'s header says why: this
|
|
11
|
+
* species set stops at C₂ where real polyolefin volatiles are C₁₀-C₄₀ waxes. Polyethylene's *molar*
|
|
12
|
+
* depolymerisation enthalpy is right — 106.6 kJ/mol against a published 92-108 — and per kilogram it
|
|
13
|
+
* is 3.8 MJ against a measured 1.8-2.2. The total heat released once the volatiles burn is exact
|
|
14
|
+
* either way; the cap moves where it is released, not how much.
|
|
15
|
+
*/
|
|
16
|
+
import { KIND_PYROLYSIS, type Reaction } from '../reaction/reaction.ts';
|
|
17
|
+
import { SHAPE_SLAB } from '../parcel/shells.ts';
|
|
18
|
+
import { massFractions } from '../substance/composition.ts';
|
|
19
|
+
import type { SubstanceThermal } from '../substance/substance.ts';
|
|
20
|
+
import type { SubstanceLibrary } from './library.ts';
|
|
21
|
+
import { COMMON_REACTIONS, GAS_REACTIONS, OXIDISE_CHAR, OXIDISE_CHAR_RICH } from './common.ts';
|
|
22
|
+
|
|
23
|
+
/** Nothing new: everything these polymers give off is already in the common set. */
|
|
24
|
+
export const POLYMER_GAS_REACTIONS: readonly string[] = GAS_REACTIONS;
|
|
25
|
+
|
|
26
|
+
const REACTIONS: readonly Reaction[] = [
|
|
27
|
+
...COMMON_REACTIONS,
|
|
28
|
+
{
|
|
29
|
+
/* Unzips to its own monomer and leaves nothing behind — measured char yield zero, and the
|
|
30
|
+
stoichiometry has no way to produce any. */
|
|
31
|
+
id: 'pyrolyse-polyethylene',
|
|
32
|
+
reactants: [{ species: 'polyethylene', moles: 1 }],
|
|
33
|
+
products: [{ species: 'C2H4', moles: 1 }],
|
|
34
|
+
kinetics: { type: 'arrhenius', A: 1e16, activationEnergy: 250000 },
|
|
35
|
+
kind: KIND_PYROLYSIS,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
/* Forty kelvin below polyethylene, because a tertiary carbon is easier to break. */
|
|
39
|
+
id: 'pyrolyse-polypropylene',
|
|
40
|
+
reactants: [{ species: 'polypropylene', moles: 1 }],
|
|
41
|
+
products: [{ species: 'C2H4', moles: 1.5 }],
|
|
42
|
+
kinetics: { type: 'arrhenius', A: 1e16, activationEnergy: 235000 },
|
|
43
|
+
kind: KIND_PYROLYSIS,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
/*
|
|
47
|
+
* Dehydrochlorination at about 275 °C, and what is left is a carbon backbone that chars.
|
|
48
|
+
* Modelled as one step rather than the real two, because the second one — the polyene charring
|
|
49
|
+
* at 400 °C — has the same products and nothing here distinguishes them.
|
|
50
|
+
*/
|
|
51
|
+
id: 'pyrolyse-pvc',
|
|
52
|
+
reactants: [{ species: 'PVC', moles: 1 }],
|
|
53
|
+
products: [
|
|
54
|
+
{ species: 'HCl', moles: 1 },
|
|
55
|
+
{ species: 'C(char)', moles: 2 },
|
|
56
|
+
{ species: 'H2', moles: 1 },
|
|
57
|
+
],
|
|
58
|
+
kinetics: { type: 'arrhenius', A: 1e9, activationEnergy: 130000 },
|
|
59
|
+
kind: KIND_PYROLYSIS,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
/* The aromatic ring survives depolymerisation and becomes char and soot, which is why a
|
|
63
|
+
polystyrene fire is the black one. */
|
|
64
|
+
id: 'pyrolyse-polystyrene',
|
|
65
|
+
reactants: [{ species: 'polystyrene', moles: 1 }],
|
|
66
|
+
products: [
|
|
67
|
+
{ species: 'C2H4', moles: 2 },
|
|
68
|
+
{ species: 'C(char)', moles: 4 },
|
|
69
|
+
],
|
|
70
|
+
kinetics: { type: 'arrhenius', A: 1e13, activationEnergy: 200000 },
|
|
71
|
+
kind: KIND_PYROLYSIS,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
/* The one polyolefin whose derived heat of gasification lands on the measurement — 1.99 MJ/kg
|
|
75
|
+
against 1.6 — because its ester group leaves as CO₂ and takes the oxygen with it. */
|
|
76
|
+
id: 'pyrolyse-pmma',
|
|
77
|
+
reactants: [{ species: 'PMMA', moles: 1 }],
|
|
78
|
+
products: [
|
|
79
|
+
{ species: 'C2H4', moles: 2 },
|
|
80
|
+
{ species: 'CO2', moles: 1 },
|
|
81
|
+
],
|
|
82
|
+
kinetics: { type: 'arrhenius', A: 1e13, activationEnergy: 190000 },
|
|
83
|
+
kind: KIND_PYROLYSIS,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
/* Two nitrogens per repeat unit, and they leave as cyanide and ammonia. Foam decomposes at
|
|
87
|
+
250-350 °C, far below any of the others, which is why furniture is the first thing to go. */
|
|
88
|
+
id: 'pyrolyse-polyurethane',
|
|
89
|
+
reactants: [{ species: 'polyurethane', moles: 1 }],
|
|
90
|
+
products: [
|
|
91
|
+
{ species: 'C(char)', moles: 12 },
|
|
92
|
+
{ species: 'CO', moles: 0.8 },
|
|
93
|
+
{ species: 'CO2', moles: 2 },
|
|
94
|
+
{ species: 'CH4', moles: 9 },
|
|
95
|
+
{ species: 'HCN', moles: 1.2 },
|
|
96
|
+
{ species: 'NH3', moles: 0.8 },
|
|
97
|
+
{ species: 'H2O(g)', moles: 1.2 },
|
|
98
|
+
],
|
|
99
|
+
kinetics: { type: 'arrhenius', A: 1e12, activationEnergy: 160000 },
|
|
100
|
+
kind: KIND_PYROLYSIS,
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
const CHAR = [OXIDISE_CHAR, OXIDISE_CHAR_RICH];
|
|
105
|
+
|
|
106
|
+
export const POLYMER: SubstanceLibrary = {
|
|
107
|
+
id: 'polymer',
|
|
108
|
+
species: [],
|
|
109
|
+
reactions: REACTIONS,
|
|
110
|
+
substances: (species): readonly SubstanceThermal[] => [
|
|
111
|
+
{
|
|
112
|
+
id: 'polyethylene',
|
|
113
|
+
composition: massFractions(species, { polyethylene: 1 }),
|
|
114
|
+
density: 940,
|
|
115
|
+
conductivity: { k0: 0.4, k1: 0 },
|
|
116
|
+
shells: 3,
|
|
117
|
+
shape: SHAPE_SLAB,
|
|
118
|
+
emissivity: 0.92,
|
|
119
|
+
reactions: ['pyrolyse-polyethylene'],
|
|
120
|
+
ignition: {
|
|
121
|
+
pilotedSurfaceK: 613,
|
|
122
|
+
autoSurfaceK: 623,
|
|
123
|
+
criticalMassFlux: 0.011,
|
|
124
|
+
limitingOxygen: 0.174,
|
|
125
|
+
},
|
|
126
|
+
appearance: {
|
|
127
|
+
albedo: [0.78, 0.78, 0.76],
|
|
128
|
+
charAlbedo: [0.05, 0.05, 0.05],
|
|
129
|
+
sootYield: 0.06,
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
id: 'polypropylene',
|
|
134
|
+
composition: massFractions(species, { polypropylene: 1 }),
|
|
135
|
+
density: 905,
|
|
136
|
+
conductivity: { k0: 0.22, k1: 0 },
|
|
137
|
+
shells: 3,
|
|
138
|
+
shape: SHAPE_SLAB,
|
|
139
|
+
emissivity: 0.92,
|
|
140
|
+
reactions: ['pyrolyse-polypropylene'],
|
|
141
|
+
ignition: {
|
|
142
|
+
pilotedSurfaceK: 610,
|
|
143
|
+
autoSurfaceK: 660,
|
|
144
|
+
criticalMassFlux: 0.011,
|
|
145
|
+
limitingOxygen: 0.175,
|
|
146
|
+
},
|
|
147
|
+
appearance: {
|
|
148
|
+
albedo: [0.76, 0.76, 0.74],
|
|
149
|
+
charAlbedo: [0.05, 0.05, 0.05],
|
|
150
|
+
sootYield: 0.06,
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
/* **The limiting oxygen index is the whole substance.** 0.45 against air's 0.209, so a flame
|
|
155
|
+
cannot be sustained on it at all and it stops the moment the pilot does. */
|
|
156
|
+
id: 'pvc',
|
|
157
|
+
composition: massFractions(species, { PVC: 1 }),
|
|
158
|
+
density: 1400,
|
|
159
|
+
conductivity: { k0: 0.19, k1: 0 },
|
|
160
|
+
shells: 3,
|
|
161
|
+
shape: SHAPE_SLAB,
|
|
162
|
+
emissivity: 0.93,
|
|
163
|
+
reactions: ['pyrolyse-pvc', ...CHAR],
|
|
164
|
+
ignition: {
|
|
165
|
+
pilotedSurfaceK: 664,
|
|
166
|
+
autoSurfaceK: 727,
|
|
167
|
+
criticalMassFlux: 0.016,
|
|
168
|
+
limitingOxygen: 0.45,
|
|
169
|
+
},
|
|
170
|
+
appearance: {
|
|
171
|
+
albedo: [0.6, 0.6, 0.58],
|
|
172
|
+
charAlbedo: [0.03, 0.03, 0.03],
|
|
173
|
+
sootYield: 0.17,
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
id: 'polystyrene',
|
|
178
|
+
composition: massFractions(species, { polystyrene: 1 }),
|
|
179
|
+
density: 1050,
|
|
180
|
+
conductivity: { k0: 0.16, k1: 0 },
|
|
181
|
+
shells: 3,
|
|
182
|
+
shape: SHAPE_SLAB,
|
|
183
|
+
emissivity: 0.92,
|
|
184
|
+
reactions: ['pyrolyse-polystyrene', ...CHAR],
|
|
185
|
+
ignition: {
|
|
186
|
+
pilotedSurfaceK: 630,
|
|
187
|
+
autoSurfaceK: 763,
|
|
188
|
+
criticalMassFlux: 0.013,
|
|
189
|
+
limitingOxygen: 0.18,
|
|
190
|
+
},
|
|
191
|
+
appearance: {
|
|
192
|
+
albedo: [0.84, 0.84, 0.82],
|
|
193
|
+
charAlbedo: [0.03, 0.03, 0.03],
|
|
194
|
+
sootYield: 0.16,
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
id: 'pmma',
|
|
199
|
+
composition: massFractions(species, { PMMA: 1 }),
|
|
200
|
+
density: 1180,
|
|
201
|
+
conductivity: { k0: 0.19, k1: 0 },
|
|
202
|
+
shells: 3,
|
|
203
|
+
shape: SHAPE_SLAB,
|
|
204
|
+
emissivity: 0.93,
|
|
205
|
+
reactions: ['pyrolyse-pmma'],
|
|
206
|
+
ignition: {
|
|
207
|
+
pilotedSurfaceK: 651,
|
|
208
|
+
autoSurfaceK: 723,
|
|
209
|
+
criticalMassFlux: 0.0044,
|
|
210
|
+
limitingOxygen: 0.173,
|
|
211
|
+
},
|
|
212
|
+
appearance: {
|
|
213
|
+
albedo: [0.88, 0.88, 0.88],
|
|
214
|
+
charAlbedo: [0.1, 0.1, 0.1],
|
|
215
|
+
sootYield: 0.022,
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
/* Thirty kilograms a cubic metre and a conductivity ten times lower than the solid: a foam is
|
|
220
|
+
mostly air, which is why it heats through in seconds and why a sofa goes up so fast. */
|
|
221
|
+
id: 'polyurethane-foam',
|
|
222
|
+
composition: massFractions(species, { polyurethane: 1 }),
|
|
223
|
+
density: 30,
|
|
224
|
+
conductivity: { k0: 0.026, k1: 0.00005 },
|
|
225
|
+
shells: 3,
|
|
226
|
+
shape: SHAPE_SLAB,
|
|
227
|
+
porosity: 0.95,
|
|
228
|
+
emissivity: 0.9,
|
|
229
|
+
reactions: ['pyrolyse-polyurethane', ...CHAR],
|
|
230
|
+
ignition: {
|
|
231
|
+
pilotedSurfaceK: 553,
|
|
232
|
+
autoSurfaceK: 673,
|
|
233
|
+
criticalMassFlux: 0.0045,
|
|
234
|
+
limitingOxygen: 0.165,
|
|
235
|
+
},
|
|
236
|
+
appearance: {
|
|
237
|
+
albedo: [0.8, 0.74, 0.55],
|
|
238
|
+
charAlbedo: [0.03, 0.03, 0.03],
|
|
239
|
+
sootYield: 0.19,
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-dimensional conduction inward through a parcel's shells.
|
|
3
|
+
*
|
|
4
|
+
* **Explicit, sub-stepped, and in enthalpy rather than temperature** — and that last part is what
|
|
5
|
+
* rules out the obvious alternative. A tridiagonal implicit solve would be unconditionally stable
|
|
6
|
+
* and is twenty lines, but it solves for *temperature*, and on a phase plateau temperature does not
|
|
7
|
+
* respond to heat at all. An implicit step there is asking a solver to invert an infinite heat
|
|
8
|
+
* capacity. The enthalpy formulation is the reason this model works, so the integrator bends to it.
|
|
9
|
+
*
|
|
10
|
+
* **The plateau helps rather than hurts.** A shell part-way through melting absorbs heat without
|
|
11
|
+
* changing temperature, which is the most stable thing a cell can do — the stiffness is all in the
|
|
12
|
+
* dry segments, where the sub-step estimate below is what handles it.
|
|
13
|
+
*
|
|
14
|
+
* **Energy is conserved exactly, whatever the step does.** Conduction *moves* energy: whatever
|
|
15
|
+
* leaves shell `i` enters shell `i + 1`, computed once and applied to both. An under-resolved step
|
|
16
|
+
* can oscillate, and it can never gain or lose a joule. That is why an explicit integrator is safe
|
|
17
|
+
* here, and `conduction.test.ts` asserts it over five thousand ticks rather than arguing it.
|
|
18
|
+
*
|
|
19
|
+
* **The interface conductivity is the harmonic mean**, because a boundary between two materials is
|
|
20
|
+
* a series resistance and series resistances add reciprocals. It matters as soon as the two sides
|
|
21
|
+
* differ — and they will: char is roughly half the conductivity of the wood beneath it, which is
|
|
22
|
+
* the whole late-stage story of a burning log. An arithmetic mean would make the char blanket
|
|
23
|
+
* insulate far less than it does.
|
|
24
|
+
*/
|
|
25
|
+
import { STANDARD_TEMPERATURE } from '../species/species.ts';
|
|
26
|
+
import { temperatureFromCapacity } from '../species/thermal.ts';
|
|
27
|
+
import type { SubstanceReactions } from '../reaction/solver.ts';
|
|
28
|
+
import type { ShellGeometry } from './shells.ts';
|
|
29
|
+
import { MAX_SHELLS } from './shells.ts';
|
|
30
|
+
|
|
31
|
+
/** W/(m·K): `k0` at the standard state, rising by `k1` per kelvin. */
|
|
32
|
+
export interface Conductivity {
|
|
33
|
+
readonly k0: number;
|
|
34
|
+
readonly k1: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Everything about conduction that is a property of the substance, resolved once at registration. */
|
|
38
|
+
export interface ConductionModel {
|
|
39
|
+
readonly geometry: ShellGeometry;
|
|
40
|
+
readonly conductivity: Conductivity | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The most sub-steps one call will take.
|
|
45
|
+
*
|
|
46
|
+
* Past this the step is under-resolved, and what happens then is decided by the clamp below rather
|
|
47
|
+
* than by this number: a pair may not exchange more heat than would equalise it, so an
|
|
48
|
+
* under-resolved step relaxes toward the right answer instead of overshooting past it.
|
|
49
|
+
*
|
|
50
|
+
* **This note used to say the failure was a ring and that a ring was the right failure.** It is not.
|
|
51
|
+
* A copper coin on the hearth in `demo/dev/chemistry.ts` — 400 W/(m·K) across a 0.3 mm shell, which
|
|
52
|
+
* wants seven hundred sub-steps — reached 4,000 K on its first step and 100 K a minute later, and
|
|
53
|
+
* *the energy was conserved the whole time*. Conservation is not enough: an ignition criterion reads
|
|
54
|
+
* a surface temperature and `present/` draws a glow from one, so an oscillation nothing loses energy
|
|
55
|
+
* to is still a fire that starts in the wrong place.
|
|
56
|
+
*/
|
|
57
|
+
const MAX_SUBSTEPS = 64;
|
|
58
|
+
|
|
59
|
+
/* Module-scope scratch, so a tick allocates nothing. Not reentrant, and nothing here nests. */
|
|
60
|
+
const temperature = new Float64Array(MAX_SHELLS);
|
|
61
|
+
const coefficient = new Float64Array(MAX_SHELLS);
|
|
62
|
+
/** Each shell's `dh/dT`, so the clamp knows what equalising a pair would cost. */
|
|
63
|
+
const capacity = new Float64Array(MAX_SHELLS);
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Move heat inward one step. `enthalpy` holds this parcel's shells at `offset`, in joules.
|
|
67
|
+
*
|
|
68
|
+
* @param shellMass kg in each shell — equal by construction, which is what equal-mass shells buy
|
|
69
|
+
* @param area m² of exposed surface
|
|
70
|
+
* @param depth m, the characteristic depth: `depthFactor × volume / area`
|
|
71
|
+
*/
|
|
72
|
+
export function conductShells(
|
|
73
|
+
enthalpy: Float64Array,
|
|
74
|
+
offset: number,
|
|
75
|
+
model: ConductionModel,
|
|
76
|
+
thermal: SubstanceReactions,
|
|
77
|
+
composition: Float64Array,
|
|
78
|
+
compositionOffset: number,
|
|
79
|
+
shellMass: number,
|
|
80
|
+
area: number,
|
|
81
|
+
depth: number,
|
|
82
|
+
dt: number,
|
|
83
|
+
): void {
|
|
84
|
+
const { geometry, conductivity } = model;
|
|
85
|
+
const count = geometry.count;
|
|
86
|
+
if (count < 2 || conductivity === null || dt <= 0) return;
|
|
87
|
+
|
|
88
|
+
/* Each shell's temperature from **its own** composition, which is what CH-6 bought: a shell part
|
|
89
|
+
way through charring has a different heat capacity from the one beside it, and a single curve
|
|
90
|
+
built at registration could not know. */
|
|
91
|
+
const width = thermal.speciesCount;
|
|
92
|
+
let smallestCapacity = Number.POSITIVE_INFINITY;
|
|
93
|
+
const read = (): void => {
|
|
94
|
+
smallestCapacity = Number.POSITIVE_INFINITY;
|
|
95
|
+
for (let i = 0; i < count; i++) {
|
|
96
|
+
const base = compositionOffset + i * width;
|
|
97
|
+
let A = 0;
|
|
98
|
+
let B = 0;
|
|
99
|
+
for (let s = 0; s < width; s++) {
|
|
100
|
+
const mass = composition[base + s] as number;
|
|
101
|
+
if (mass === 0) continue;
|
|
102
|
+
A += mass * (thermal.cpA[s] as number);
|
|
103
|
+
B += mass * (thermal.cpB[s] as number);
|
|
104
|
+
}
|
|
105
|
+
temperature[i] = temperatureFromCapacity(A, B, enthalpy[offset + i] as number);
|
|
106
|
+
/* `dh/dT` at this shell's own temperature, which is `A + B·ΔT` for a `cp` linear in `T`. */
|
|
107
|
+
const rise = (temperature[i] as number) - STANDARD_TEMPERATURE;
|
|
108
|
+
const slope = A + B * rise;
|
|
109
|
+
capacity[i] = slope > 0 ? slope : A > 0 ? A : shellMass;
|
|
110
|
+
if (A > 0 && A < smallestCapacity) smallestCapacity = A;
|
|
111
|
+
}
|
|
112
|
+
if (!Number.isFinite(smallestCapacity)) smallestCapacity = shellMass;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/* Conductances, W/K, from the temperatures the step starts at. Recomputed per sub-step, because
|
|
116
|
+
the whole point of a temperature-dependent `k` is that it moves as the front does. */
|
|
117
|
+
const conductances = (): number => {
|
|
118
|
+
let worst = 0;
|
|
119
|
+
for (let i = 0; i < count - 1; i++) {
|
|
120
|
+
const kHot =
|
|
121
|
+
conductivity.k0 + conductivity.k1 * ((temperature[i] as number) - STANDARD_TEMPERATURE);
|
|
122
|
+
const kCold =
|
|
123
|
+
conductivity.k0 + conductivity.k1 * ((temperature[i + 1] as number) - STANDARD_TEMPERATURE);
|
|
124
|
+
const total = kHot + kCold;
|
|
125
|
+
const effective = total > 0 ? (2 * kHot * kCold) / total : 0;
|
|
126
|
+
const face = area * (geometry.areaFactor[i] as number);
|
|
127
|
+
const span =
|
|
128
|
+
depth *
|
|
129
|
+
(((geometry.thicknessFraction[i] as number) +
|
|
130
|
+
(geometry.thicknessFraction[i + 1] as number)) /
|
|
131
|
+
2);
|
|
132
|
+
const conductance = span > 0 ? (effective * face) / span : 0;
|
|
133
|
+
coefficient[i] = conductance;
|
|
134
|
+
if (conductance > worst) worst = conductance;
|
|
135
|
+
}
|
|
136
|
+
return worst;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
read();
|
|
140
|
+
const worst = conductances();
|
|
141
|
+
if (!(worst > 0)) return;
|
|
142
|
+
|
|
143
|
+
/*
|
|
144
|
+
* The explicit stability limit, as a sub-step count.
|
|
145
|
+
*
|
|
146
|
+
* A shell's temperature responds to a flux at `1 / (m·c)`, so a step is resolved while
|
|
147
|
+
* `dt · conductance / (m · c) ≤ 1/2`. `minHeatCapacity` is the smallest `dh/dT` anywhere on the
|
|
148
|
+
* substance's curve, which makes this conservative everywhere and exact at the worst point.
|
|
149
|
+
*/
|
|
150
|
+
/* The sub-step estimate uses the smallest heat capacity actually present, which is the shell
|
|
151
|
+
that responds fastest to a flux and therefore the one that sets the limit. */
|
|
152
|
+
const limit = (2 * dt * worst) / smallestCapacity;
|
|
153
|
+
const substeps = Math.min(MAX_SUBSTEPS, Math.max(1, Math.ceil(limit)));
|
|
154
|
+
const step = dt / substeps;
|
|
155
|
+
|
|
156
|
+
for (let pass = 0; pass < substeps; pass++) {
|
|
157
|
+
if (pass > 0) {
|
|
158
|
+
read();
|
|
159
|
+
conductances();
|
|
160
|
+
}
|
|
161
|
+
for (let i = 0; i < count - 1; i++) {
|
|
162
|
+
const gap = (temperature[i] as number) - (temperature[i + 1] as number);
|
|
163
|
+
let joules = (coefficient[i] as number) * gap * step;
|
|
164
|
+
/*
|
|
165
|
+
* **Clamped at the point the two shells would be equal**, which is unconditionally stable and
|
|
166
|
+
* changes nothing whenever the step was resolved anyway.
|
|
167
|
+
*
|
|
168
|
+
* Heat moves down a temperature difference until there is no difference left; a step that
|
|
169
|
+
* would carry the cold shell *past* the hot one has moved more heat than exists to move, and
|
|
170
|
+
* doing that repeatedly is the oscillation `MAX_SUBSTEPS` used to permit. The equalising
|
|
171
|
+
* amount for two capacities in series is `ΔT · CaCb/(Ca+Cb)`, and it is the same arithmetic
|
|
172
|
+
* two conductivities in series already use.
|
|
173
|
+
*/
|
|
174
|
+
const ca = capacity[i] as number;
|
|
175
|
+
const cb = capacity[i + 1] as number;
|
|
176
|
+
const total = ca + cb;
|
|
177
|
+
if (total > 0) {
|
|
178
|
+
const equalising = (gap * ca * cb) / total;
|
|
179
|
+
if (joules > 0 && joules > equalising) joules = equalising;
|
|
180
|
+
else if (joules < 0 && joules < equalising) joules = equalising;
|
|
181
|
+
}
|
|
182
|
+
/* Computed once and applied to both, which is what makes conservation exact rather than
|
|
183
|
+
approximate: the same double is subtracted here and added there. */
|
|
184
|
+
enthalpy[offset + i] = (enthalpy[offset + i] as number) - joules;
|
|
185
|
+
enthalpy[offset + i + 1] = (enthalpy[offset + i + 1] as number) + joules;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A parcel's depth, as nested shells of equal mass.
|
|
3
|
+
*
|
|
4
|
+
* **Without this a fire model cannot be right, and `§4` is the argument.** The time for a heat
|
|
5
|
+
* front to reach depth `d` goes as `ρ·c·d²/k`. For wood that is about six seconds at one
|
|
6
|
+
* millimetre and four hours at fifty, so a parcel with a single temperature is wrong by that whole
|
|
7
|
+
* factor — in the direction that flashes every log in the world over at once. Thin things ignite
|
|
8
|
+
* and thick things do not, and nothing else in the model produces that.
|
|
9
|
+
*
|
|
10
|
+
* **Equal mass rather than equal thickness**, which for a cylinder or a sphere puts the resolution
|
|
11
|
+
* where the gradient is: the outer shell of a large object is thin and the core is fat. For a slab
|
|
12
|
+
* the two are the same thing, because a slab's cross-section does not shrink inward.
|
|
13
|
+
*
|
|
14
|
+
* Shell 0 is the surface and shell `count - 1` is the core.
|
|
15
|
+
*
|
|
16
|
+
* The fractions here are pure functions of a shape and a count, so they are computed **once per
|
|
17
|
+
* substance at registration** and multiplied by a parcel's own depth at use. That is not tidiness:
|
|
18
|
+
* `Math.cbrt` is not exactly specified by ECMAScript, and `§15` keeps every operation the
|
|
19
|
+
* specification declines to pin down off the simulation path.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export const SHAPE_SLAB = 0;
|
|
23
|
+
export const SHAPE_CYLINDER = 1;
|
|
24
|
+
export const SHAPE_SPHERE = 2;
|
|
25
|
+
|
|
26
|
+
export type Shape = typeof SHAPE_SLAB | typeof SHAPE_CYLINDER | typeof SHAPE_SPHERE;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The most shells a parcel may have.
|
|
30
|
+
*
|
|
31
|
+
* Sixteen because that is the design's `Hero` tier in `§14`, and because a parcel's enthalpy column
|
|
32
|
+
* is strided by this constant: every parcel pays sixteen doubles whether it uses them or not, which
|
|
33
|
+
* is 128 bytes and is the price of not needing a per-parcel offset table.
|
|
34
|
+
*/
|
|
35
|
+
export const MAX_SHELLS = 16;
|
|
36
|
+
|
|
37
|
+
export interface ShellGeometry {
|
|
38
|
+
readonly shape: Shape;
|
|
39
|
+
readonly count: number;
|
|
40
|
+
/** Radius of shell `i`'s **inner** boundary, as a fraction of the characteristic depth. */
|
|
41
|
+
readonly innerFraction: Float64Array;
|
|
42
|
+
/** Area of that boundary, as a fraction of the exposed surface area. */
|
|
43
|
+
readonly areaFactor: Float64Array;
|
|
44
|
+
/** Shell `i`'s own thickness, as a fraction of the characteristic depth. These sum to one. */
|
|
45
|
+
readonly thicknessFraction: Float64Array;
|
|
46
|
+
/**
|
|
47
|
+
* Characteristic depth is this times volume over area: 1 for a slab, 2 for a cylinder, 3 for a
|
|
48
|
+
* sphere. It is what makes `V/A` mean "half-thickness" for a cylinder and "a third of the radius"
|
|
49
|
+
* for a sphere, which is the same relation fire engineering uses.
|
|
50
|
+
*/
|
|
51
|
+
readonly depthFactor: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The exponent relating enclosed volume to radius: `V ∝ r^(m+1)`. */
|
|
55
|
+
function volumeExponent(shape: Shape): number {
|
|
56
|
+
switch (shape) {
|
|
57
|
+
case SHAPE_SLAB:
|
|
58
|
+
return 1;
|
|
59
|
+
case SHAPE_CYLINDER:
|
|
60
|
+
return 2;
|
|
61
|
+
case SHAPE_SPHERE:
|
|
62
|
+
return 3;
|
|
63
|
+
default:
|
|
64
|
+
throw new Error(`shape ${shape} is not a slab, a cylinder or a sphere`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function shellGeometry(shape: Shape, count: number): ShellGeometry {
|
|
69
|
+
if (!Number.isInteger(count) || count < 1 || count > MAX_SHELLS) {
|
|
70
|
+
throw new Error(`a parcel has between 1 and ${MAX_SHELLS} shells, not ${count}`);
|
|
71
|
+
}
|
|
72
|
+
const exponent = volumeExponent(shape);
|
|
73
|
+
|
|
74
|
+
const innerFraction = new Float64Array(count);
|
|
75
|
+
const areaFactor = new Float64Array(count);
|
|
76
|
+
const thicknessFraction = new Float64Array(count);
|
|
77
|
+
|
|
78
|
+
/* Shell `i`'s inner boundary encloses `(count - 1 - i) / count` of the volume, which is what
|
|
79
|
+
equal mass means, and radius follows by the shape's own exponent. */
|
|
80
|
+
// determinism: build-time — shell geometry is computed at registration, per §15 of the design
|
|
81
|
+
const radiusAt = (volumeFraction: number): number => volumeFraction ** (1 / exponent);
|
|
82
|
+
|
|
83
|
+
for (let i = 0; i < count; i++) {
|
|
84
|
+
const inner = radiusAt((count - 1 - i) / count);
|
|
85
|
+
const outer = radiusAt((count - i) / count);
|
|
86
|
+
innerFraction[i] = inner;
|
|
87
|
+
// determinism: build-time — the same registration-time geometry as the line above
|
|
88
|
+
areaFactor[i] = inner ** (exponent - 1);
|
|
89
|
+
thicknessFraction[i] = outer - inner;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
shape,
|
|
94
|
+
count,
|
|
95
|
+
innerFraction,
|
|
96
|
+
areaFactor,
|
|
97
|
+
thicknessFraction,
|
|
98
|
+
depthFactor: exponent,
|
|
99
|
+
};
|
|
100
|
+
}
|