zeropoint-node 1.0.5 → 1.0.7
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/CHANGELOG.md +178 -0
- package/CITATION.cff +1 -1
- package/README.md +50 -10
- package/dist/legacy-BEa7tljT.js.map +1 -1
- package/dist/legacy-CV8oTnKM.cjs.map +1 -1
- package/package.json +10 -2
- package/src/0/3/6/9/1/2/4/8/7/5/1/a432.breathe.ts +15 -1
- package/src/0/3/6/9/1/2/4/8/7/5/1/a432.cleanup.ts +15 -1
- package/src/0/3/6/9/1/2/4/8/7/5/1/a432.cmyk.ts +7 -1
- package/src/0/3/6/9/1/2/4/8/7/5/1/a432.generate.matrix.ts +15 -1
- package/src/0/3/6/9/1/2/4/8/7/5/1/a432.i.browse.ts +15 -1
- package/src/0/3/6/9/1/2/4/8/7/5/1/a432.i.click.ts +15 -1
- package/src/0/3/6/9/1/2/4/8/7/5/1/a432.i.hear.ts +15 -1
- package/src/0/3/6/9/1/2/4/8/7/5/1/a432.i.heat.ts +15 -1
- package/src/0/3/6/9/1/2/4/8/7/5/1/a432.i.pulse.ts +15 -1
- package/src/0/3/6/9/1/2/4/8/7/5/1/a432.i.speak.ts +15 -1
- package/src/0/3/6/9/1/2/4/8/7/5/1/a432.i.tap.ts +15 -1
- package/src/0/3/6/9/1/2/4/8/7/5/1/a432.math.ts +14 -6
- package/src/0/3/6/9/1/2/4/8/7/5/1/a432.overlay.visual.controller.ts +0 -3
- package/src/0/3/6/9/1/2/4/8/7/5/1/a432.rodin.ts +15 -1
- package/src/0/3/6/9/1/2/4/8/7/5/1/a432.self.ts +15 -1
- package/src/0/3/6/9/1/2/4/8/7/5/1/a432.service.worker.ts +30 -7
- package/src/0/3/6/9/1/2/4/8/7/5/1/a432.terminal.ts +15 -1
- package/src/0/3/6/9/1/2/4/8/7/5/1/a432.test.ts +15 -1
- package/src/0/3/6/9/1/2/4/8/7/5/1/a432.trinity.ts +0 -3
- package/src/0/3/6/9/1/2/4/8/7/5/1/a432.vbm.path.ts +6 -2
- package/src/0/3/6/9/1/2/4/8/7/5/1/a432.vbm.relations.ts +15 -1
- package/src/0/index.ts +16 -0
- package/src/index.ts +4 -4
- package/src/kernel/import-graph.ts +58 -0
- package/src/multidimensional-vortex-demo.ts +1 -1
- package/src/quantum/superposition-execution.ts +9 -4
- package/src/thermo/free-energy.ts +279 -0
- package/src/thermo/wastewater-energy.ts +204 -0
- package/src/vbm-math.ts +15 -1
- package/src/verification/lean-bridge.ts +384 -0
- package/src/zero-entropy-demo.ts +15 -1
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Free energy of the water reaction — Gibbs, exactly.
|
|
3
|
+
*
|
|
4
|
+
* "Free energy" is not a loose phrase. It is ΔG, the portion of a reaction's
|
|
5
|
+
* enthalpy change that can be taken out as work at constant temperature and
|
|
6
|
+
* pressure, with TΔS the part that cannot because it is owed to entropy:
|
|
7
|
+
*
|
|
8
|
+
* ΔG = ΔH − TΔS
|
|
9
|
+
*
|
|
10
|
+
* Its sign is the whole answer to whether a process can be run for gain. A
|
|
11
|
+
* reaction with ΔG < 0 releases work and runs on its own; one with ΔG > 0 must
|
|
12
|
+
* be paid for. For splitting water, ΔG is POSITIVE, and the size of it is the
|
|
13
|
+
* bill: 237.14 kJ per mole, or 1.229 V across an electrolysis cell. Nothing in
|
|
14
|
+
* this module is an opinion about that — every figure recomputes from three
|
|
15
|
+
* tabulated standard quantities.
|
|
16
|
+
*
|
|
17
|
+
* ARITHMETIC. All values are exact integers in units of 10⁻⁵ J/mol, so nothing
|
|
18
|
+
* here is a float literal and no rounding happens until a value is rendered.
|
|
19
|
+
* The constants are standard-state (298.15 K, 1 bar) CODATA/NIST values.
|
|
20
|
+
*
|
|
21
|
+
* WHAT THIS SETTLES. The forward and reverse reactions share one bond ledger,
|
|
22
|
+
* so what splitting costs is exactly what burning returns — at best. Real
|
|
23
|
+
* devices take a cut at every step, which `roundTrip` reports. There is no
|
|
24
|
+
* arrangement of the two that yields more out than in, and that is not an
|
|
25
|
+
* engineering limit to be improved on: it is the sign of ΔG.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// SCALE — exact integer arithmetic, no float literals
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
/** Every energy below is an integer count of 10⁻⁵ J/mol. */
|
|
33
|
+
export const SCALE = 100000
|
|
34
|
+
|
|
35
|
+
/** Render a scaled energy as kJ/mol, rounding once, at the boundary. */
|
|
36
|
+
export function toKilojoulesPerMole(scaled: number): number {
|
|
37
|
+
return round(scaled, SCALE * 1000)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Round a/b to the nearest integer without Math.*, ties away from zero. */
|
|
41
|
+
function round(a: number, b: number): number {
|
|
42
|
+
const neg = a < 0
|
|
43
|
+
const x = neg ? -a : a
|
|
44
|
+
const q = (2 * x + b) / (2 * b)
|
|
45
|
+
const f = q - (q % 1)
|
|
46
|
+
return neg ? -f : f
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// STANDARD STATE (298.15 K, 1 bar) — CODATA / NIST
|
|
51
|
+
// ============================================================================
|
|
52
|
+
|
|
53
|
+
/** 298.15 K, held as hundredths of a kelvin. */
|
|
54
|
+
const T_CENTIKELVIN = 29815
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* ΔH° of formation for liquid water, −285.83 kJ/mol.
|
|
58
|
+
* H₂(g) + ½O₂(g) → H₂O(l)
|
|
59
|
+
*/
|
|
60
|
+
export const ENTHALPY_FORMATION = -285830 * SCALE
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* ΔS° of that formation, −163.305 J/(mol·K), from the absolute entropies
|
|
64
|
+
* S°(H₂O,l) 69.95, S°(H₂) 130.68, S°(O₂) 205.15 J/(mol·K):
|
|
65
|
+
* 69.95 − 130.68 − 205.15/2 = −163.305
|
|
66
|
+
* Held as thousandths of a J/(mol·K).
|
|
67
|
+
*/
|
|
68
|
+
const ENTROPY_FORMATION_MILLI = -163305
|
|
69
|
+
|
|
70
|
+
/** Faraday constant, C/mol. */
|
|
71
|
+
export const FARADAY = 96485
|
|
72
|
+
|
|
73
|
+
/** Electrons transferred per water molecule. */
|
|
74
|
+
export const ELECTRONS = 2
|
|
75
|
+
|
|
76
|
+
// ============================================================================
|
|
77
|
+
// THE FREE ENERGY
|
|
78
|
+
// ============================================================================
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* TΔS for the formation, in units of 10⁻⁵ J/mol.
|
|
82
|
+
*
|
|
83
|
+
* (T_CENTIKELVIN/100) × (ENTROPY_FORMATION_MILLI/1000) J/mol, and the two
|
|
84
|
+
* denominators multiply to exactly SCALE, so the product is already an integer
|
|
85
|
+
* in our units — no rounding enters here.
|
|
86
|
+
*/
|
|
87
|
+
export const ENTROPY_TERM_FORMATION = T_CENTIKELVIN * ENTROPY_FORMATION_MILLI
|
|
88
|
+
|
|
89
|
+
/** ΔG° of formation: ΔH − TΔS. Negative — water forms and releases work. */
|
|
90
|
+
export const GIBBS_FORMATION = ENTHALPY_FORMATION - ENTROPY_TERM_FORMATION
|
|
91
|
+
|
|
92
|
+
/** Splitting is the reverse reaction, so every quantity changes sign. */
|
|
93
|
+
export const ENTHALPY_SPLITTING = -ENTHALPY_FORMATION
|
|
94
|
+
export const GIBBS_SPLITTING = -GIBBS_FORMATION
|
|
95
|
+
|
|
96
|
+
// ============================================================================
|
|
97
|
+
// CELL POTENTIALS — the same numbers, in volts
|
|
98
|
+
// ============================================================================
|
|
99
|
+
|
|
100
|
+
/** An exact rational, so no precision is lost before it is rendered. */
|
|
101
|
+
export interface ExactPotential {
|
|
102
|
+
/** Microvolts, as numerator/denominator. */
|
|
103
|
+
readonly numerator: number
|
|
104
|
+
readonly denominator: number
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Reversible cell potential E° = ΔG/(nF) as an EXACT fraction of a microvolt.
|
|
109
|
+
*
|
|
110
|
+
* The rounded form below cannot be inverted exactly — quantising to whole
|
|
111
|
+
* microvolts throws away what the inverse would need. Returning the fraction
|
|
112
|
+
* keeps ΔG recoverable with no drift at all, which is what lets
|
|
113
|
+
* `every_model_inverts` demand equality rather than a tolerance.
|
|
114
|
+
*/
|
|
115
|
+
export function reversiblePotentialExact(): ExactPotential {
|
|
116
|
+
return { numerator: GIBBS_SPLITTING * 10, denominator: ELECTRONS * FARADAY }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Thermoneutral potential ΔH/(nF), likewise exact. */
|
|
120
|
+
export function thermoneutralPotentialExact(): ExactPotential {
|
|
121
|
+
return { numerator: ENTHALPY_SPLITTING * 10, denominator: ELECTRONS * FARADAY }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Recover the energy an exact potential came from, in 10⁻⁵ J/mol.
|
|
126
|
+
*
|
|
127
|
+
* E is `numerator/denominator` microvolts and energy = E·nF/10, where the
|
|
128
|
+
* denominator IS nF — so it cancels. The cancellation is done here rather than
|
|
129
|
+
* by multiplying it out: `numerator × denominator` is about 4.6e16, past the
|
|
130
|
+
* 2^53 where doubles stop counting exactly, and multiplying it out reintroduced
|
|
131
|
+
* the very drift these exact potentials exist to remove.
|
|
132
|
+
*/
|
|
133
|
+
export function energyFromPotential(p: ExactPotential): number {
|
|
134
|
+
if (p.denominator !== ELECTRONS * FARADAY) {
|
|
135
|
+
throw new RangeError(`potential denominator must be nF = ${ELECTRONS * FARADAY}, got ${p.denominator}`)
|
|
136
|
+
}
|
|
137
|
+
return p.numerator / 10
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Reversible cell potential, E° = ΔG/(nF), in whole microvolts.
|
|
142
|
+
*
|
|
143
|
+
* The least voltage that can split water at all. Below it the reaction does not
|
|
144
|
+
* run, however the cell is built. Rounded for display; use
|
|
145
|
+
* `reversiblePotentialExact` when the value will be computed with further.
|
|
146
|
+
*/
|
|
147
|
+
export function reversiblePotentialMicrovolts(): number {
|
|
148
|
+
const { numerator, denominator } = reversiblePotentialExact()
|
|
149
|
+
return round(numerator, denominator)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Thermoneutral potential, ΔH/(nF), in microvolts.
|
|
154
|
+
*
|
|
155
|
+
* At this voltage the cell neither warms nor cools: the entropy term is being
|
|
156
|
+
* supplied electrically instead of drawn from the surroundings. It is strictly
|
|
157
|
+
* above the reversible potential, and the gap is TΔS.
|
|
158
|
+
*/
|
|
159
|
+
export function thermoneutralPotentialMicrovolts(): number {
|
|
160
|
+
const { numerator, denominator } = thermoneutralPotentialExact()
|
|
161
|
+
return round(numerator, denominator)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ============================================================================
|
|
165
|
+
// THE CYCLE
|
|
166
|
+
// ============================================================================
|
|
167
|
+
|
|
168
|
+
export interface RoundTrip {
|
|
169
|
+
/** Energy that must be supplied to split, 10⁻⁵ J/mol. */
|
|
170
|
+
readonly inputRequired: number
|
|
171
|
+
/** Energy recoverable by burning the hydrogen back, 10⁻⁵ J/mol. */
|
|
172
|
+
readonly outputRecovered: number
|
|
173
|
+
/** outputRecovered − inputRequired. Never positive. */
|
|
174
|
+
readonly net: number
|
|
175
|
+
/** Efficiency as a percentage, rounded once. */
|
|
176
|
+
readonly percent: number
|
|
177
|
+
/** True only if the cycle returns more than it took. */
|
|
178
|
+
readonly gainsEnergy: boolean
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* A split-then-burn cycle, at whatever efficiencies you give it.
|
|
183
|
+
*
|
|
184
|
+
* Each efficiency is a percentage, 0..100, as an integer. Passing 100 for all
|
|
185
|
+
* three describes the thermodynamic ideal — the case most favourable to a
|
|
186
|
+
* closed loop, and the one that matters, because if it fails there it fails
|
|
187
|
+
* everywhere.
|
|
188
|
+
*/
|
|
189
|
+
export function roundTrip(
|
|
190
|
+
electrolysisPercent: number,
|
|
191
|
+
enginePercent: number,
|
|
192
|
+
generatorPercent: number,
|
|
193
|
+
): RoundTrip {
|
|
194
|
+
for (const p of [electrolysisPercent, enginePercent, generatorPercent]) {
|
|
195
|
+
if (!Number.isInteger(p) || p < 0 || p > 100) {
|
|
196
|
+
throw new RangeError(`efficiency must be an integer percentage in 0..100, got ${p}`)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Splitting must supply the full enthalpy, not merely ΔG: a real cell run at
|
|
201
|
+
// the reversible potential draws the entropy term from its surroundings and
|
|
202
|
+
// goes cold, which no engine cycle recovers.
|
|
203
|
+
const inputRequired = electrolysisPercent === 0
|
|
204
|
+
? Number.POSITIVE_INFINITY
|
|
205
|
+
: round(ENTHALPY_SPLITTING * 100, electrolysisPercent)
|
|
206
|
+
|
|
207
|
+
// Burning returns the same enthalpy — the identical bond ledger, reversed —
|
|
208
|
+
// and then the engine and generator each take their cut.
|
|
209
|
+
const outputRecovered = round(
|
|
210
|
+
round(ENTHALPY_FORMATION * -1 * enginePercent, 100) * generatorPercent,
|
|
211
|
+
100,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
const net = outputRecovered - inputRequired
|
|
215
|
+
const percent = inputRequired === Number.POSITIVE_INFINITY
|
|
216
|
+
? 0
|
|
217
|
+
: round(outputRecovered * 100, inputRequired)
|
|
218
|
+
|
|
219
|
+
return { inputRequired, outputRecovered, net, percent, gainsEnergy: net > 0 }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ============================================================================
|
|
223
|
+
// SELF-CHECK
|
|
224
|
+
// ============================================================================
|
|
225
|
+
|
|
226
|
+
/** Facts this module must satisfy. Returns failures. */
|
|
227
|
+
export function selfTest(): string[] {
|
|
228
|
+
const fail: string[] = []
|
|
229
|
+
|
|
230
|
+
// ΔG = ΔH − TΔS must reproduce the tabulated ΔG°f of −237.14 kJ/mol.
|
|
231
|
+
const kj = toKilojoulesPerMole(GIBBS_FORMATION)
|
|
232
|
+
if (kj !== -237) fail.push(`ΔG°f rounds to ${kj} kJ/mol, expected -237`)
|
|
233
|
+
|
|
234
|
+
// Formation releases work; splitting costs it. This is the sign that decides
|
|
235
|
+
// whether a reaction can be run for gain, and it is the whole answer.
|
|
236
|
+
if (!(GIBBS_FORMATION < 0)) fail.push('ΔG of formation is not negative')
|
|
237
|
+
if (!(GIBBS_SPLITTING > 0)) fail.push('ΔG of splitting is not positive')
|
|
238
|
+
|
|
239
|
+
// The forward and reverse reactions are one ledger read in two directions.
|
|
240
|
+
if (ENTHALPY_SPLITTING !== -ENTHALPY_FORMATION) fail.push('enthalpy is not antisymmetric')
|
|
241
|
+
if (GIBBS_SPLITTING !== -GIBBS_FORMATION) fail.push('Gibbs is not antisymmetric')
|
|
242
|
+
|
|
243
|
+
// Cell potentials, to the millivolt.
|
|
244
|
+
const rev = round(reversiblePotentialMicrovolts(), 1000)
|
|
245
|
+
const thermo = round(thermoneutralPotentialMicrovolts(), 1000)
|
|
246
|
+
if (rev !== 1229) fail.push(`reversible potential ${rev} mV, expected 1229`)
|
|
247
|
+
if (thermo !== 1481) fail.push(`thermoneutral potential ${thermo} mV, expected 1481`)
|
|
248
|
+
if (!(thermo > rev)) fail.push('thermoneutral potential is not above the reversible one')
|
|
249
|
+
|
|
250
|
+
// The exact potentials must invert with NO drift — that is their purpose.
|
|
251
|
+
if (energyFromPotential(reversiblePotentialExact()) !== GIBBS_SPLITTING) {
|
|
252
|
+
fail.push('reversible potential does not invert exactly')
|
|
253
|
+
}
|
|
254
|
+
if (energyFromPotential(thermoneutralPotentialExact()) !== ENTHALPY_SPLITTING) {
|
|
255
|
+
fail.push('thermoneutral potential does not invert exactly')
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// The ideal cycle breaks even and never better.
|
|
259
|
+
const ideal = roundTrip(100, 100, 100)
|
|
260
|
+
if (ideal.net !== 0) fail.push(`ideal cycle net ${ideal.net}, expected 0`)
|
|
261
|
+
if (ideal.gainsEnergy) fail.push('ideal cycle reports a gain')
|
|
262
|
+
|
|
263
|
+
// Every real cycle loses. Exhaustive over the efficiency grid in 5% steps,
|
|
264
|
+
// excluding only the ideal corner above.
|
|
265
|
+
for (let e = 5; e <= 100; e += 5) {
|
|
266
|
+
for (let m = 5; m <= 100; m += 5) {
|
|
267
|
+
for (let g = 5; g <= 100; g += 5) {
|
|
268
|
+
if (e === 100 && m === 100 && g === 100) continue
|
|
269
|
+
const r = roundTrip(e, m, g)
|
|
270
|
+
if (r.gainsEnergy) {
|
|
271
|
+
fail.push(`cycle at ${e}/${m}/${g}% reports a gain of ${r.net}`)
|
|
272
|
+
return fail
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return fail
|
|
279
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Polluted water in, electricity and drinkable water out — the real version.
|
|
3
|
+
*
|
|
4
|
+
* The described machine exists. What makes it work is that the fuel is not the
|
|
5
|
+
* water: it is what is dissolved in it. H₂O carries no recoverable energy —
|
|
6
|
+
* `free-energy.ts` settles that, ΔG for splitting is +237 kJ/mol — but organic
|
|
7
|
+
* contamination does, and a litre of strong effluent can carry more chemical
|
|
8
|
+
* energy than it costs to clean the litre.
|
|
9
|
+
*
|
|
10
|
+
* So the architecture is:
|
|
11
|
+
*
|
|
12
|
+
* polluted water → anaerobic digestion → biogas (mostly CH₄)
|
|
13
|
+
* → combustion engine (CHP) → ELECTRICITY
|
|
14
|
+
* digester effluent → membrane treatment → DRINKABLE WATER
|
|
15
|
+
*
|
|
16
|
+
* The engine burns methane the bacteria made from the pollutants. Its exhaust
|
|
17
|
+
* is CO₂ and water vapour; the drinkable water comes from the membrane train,
|
|
18
|
+
* powered by part of the electricity. Everything you described happens, and
|
|
19
|
+
* nothing runs on the water itself.
|
|
20
|
+
*
|
|
21
|
+
* THE WHOLE QUESTION IS THE LOAD. Weak sewage does not carry enough energy to
|
|
22
|
+
* pay for its own treatment; strong industrial effluent carries several times
|
|
23
|
+
* over. There is therefore a THRESHOLD contamination, and this module computes
|
|
24
|
+
* it rather than asserting it. Below it the plant imports power; above it the
|
|
25
|
+
* plant exports.
|
|
26
|
+
*
|
|
27
|
+
* Distillation is deliberately not the water step. Evaporating a litre costs
|
|
28
|
+
* about 2.26 MJ in latent heat alone — far more than any realistic load carries
|
|
29
|
+
* — so a still cannot be paid for by the pollution. Membranes can.
|
|
30
|
+
*
|
|
31
|
+
* ARITHMETIC. Integers throughout, in decijoules per litre (dJ/L). No float
|
|
32
|
+
* literals; rounding happens once, where a value is rendered.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// CONSTANTS — measured quantities, not choices
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Chemical energy per unit of COD (chemical oxygen demand): 13.9 J per mg.
|
|
41
|
+
* COD is the standard measure of oxidisable load, so this converts "how dirty"
|
|
42
|
+
* directly into "how much energy is in it". Held as tenths of a J/mg.
|
|
43
|
+
*/
|
|
44
|
+
const JOULES_PER_MG_COD_TENTHS = 139
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The same figure as J/mg, for prose that quotes it. Derived from the tenths
|
|
48
|
+
* above rather than written as 13.9 — a decimal literal here would be a second
|
|
49
|
+
* copy of the constant, and the ratchet counts those for exactly that reason.
|
|
50
|
+
*/
|
|
51
|
+
export const JOULES_PER_MG_COD = JOULES_PER_MG_COD_TENTHS / 10
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Fraction of COD that anaerobic digestion converts to methane, as a
|
|
55
|
+
* percentage. Well-run mesophilic digesters on soluble organics reach the
|
|
56
|
+
* mid-sixties; the rest becomes biomass or leaves undigested.
|
|
57
|
+
*/
|
|
58
|
+
const DIGESTION_CAPTURE_PERCENT = 65
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Electrical efficiency of a gas engine in CHP service, as a percentage.
|
|
62
|
+
* Reciprocating engines in the hundreds-of-kW class sit near this. Heat is
|
|
63
|
+
* recovered too, but heat is not what was asked for, so it is not counted.
|
|
64
|
+
*/
|
|
65
|
+
const ENGINE_ELECTRICAL_PERCENT = 38
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Electricity to bring digester effluent to drinking standard by membranes:
|
|
69
|
+
* ultrafiltration, reverse osmosis, then polishing. 4 kWh per m³ = 4 Wh/L =
|
|
70
|
+
* 14 400 J/L = 144 000 dJ/L. The conservative end of the RO range, because a
|
|
71
|
+
* dirty feed fouls membranes and costs more than a clean one.
|
|
72
|
+
*/
|
|
73
|
+
export const TREATMENT_DEMAND_DECIJOULES_PER_LITRE = 144000
|
|
74
|
+
|
|
75
|
+
// ============================================================================
|
|
76
|
+
// THE BALANCE
|
|
77
|
+
// ============================================================================
|
|
78
|
+
|
|
79
|
+
export interface WaterBalance {
|
|
80
|
+
/** Contamination as COD, mg/L — the input. */
|
|
81
|
+
readonly codMilligramsPerLitre: number
|
|
82
|
+
/** Chemical energy present in one litre, dJ/L. */
|
|
83
|
+
readonly energyInLoad: number
|
|
84
|
+
/** Electricity the engine actually delivers, dJ/L. */
|
|
85
|
+
readonly electricityGenerated: number
|
|
86
|
+
/** Electricity the membrane train consumes, dJ/L. */
|
|
87
|
+
readonly treatmentDemand: number
|
|
88
|
+
/** generated − demand. Positive means the plant exports power. */
|
|
89
|
+
readonly netElectricity: number
|
|
90
|
+
/** True when the water pays for its own cleaning and more. */
|
|
91
|
+
readonly selfPowering: boolean
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Integer division rounded to nearest, ties away from zero, no Math.*. */
|
|
95
|
+
function round(a: number, b: number): number {
|
|
96
|
+
const neg = a < 0
|
|
97
|
+
const x = neg ? -a : a
|
|
98
|
+
const q = (2 * x + b) / (2 * b)
|
|
99
|
+
const f = q - (q % 1)
|
|
100
|
+
return neg ? -f : f
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Energy balance for one litre at a given contamination.
|
|
105
|
+
*
|
|
106
|
+
* `cod` is chemical oxygen demand in mg/L: roughly 300–800 for municipal
|
|
107
|
+
* sewage, 2 000–6 000 for dairy or brewery effluent, 20 000–80 000 for manure
|
|
108
|
+
* slurry or food-processing waste.
|
|
109
|
+
*/
|
|
110
|
+
export function balanceFor(cod: number): WaterBalance {
|
|
111
|
+
if (!Number.isInteger(cod) || cod < 0) {
|
|
112
|
+
throw new RangeError(`COD must be a non-negative integer in mg/L, got ${cod}`)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const energyInLoad = cod * JOULES_PER_MG_COD_TENTHS
|
|
116
|
+
const captured = round(energyInLoad * DIGESTION_CAPTURE_PERCENT, 100)
|
|
117
|
+
const electricityGenerated = round(captured * ENGINE_ELECTRICAL_PERCENT, 100)
|
|
118
|
+
const netElectricity = electricityGenerated - TREATMENT_DEMAND_DECIJOULES_PER_LITRE
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
codMilligramsPerLitre: cod,
|
|
122
|
+
energyInLoad,
|
|
123
|
+
electricityGenerated,
|
|
124
|
+
treatmentDemand: TREATMENT_DEMAND_DECIJOULES_PER_LITRE,
|
|
125
|
+
netElectricity,
|
|
126
|
+
selfPowering: netElectricity > 0,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The lowest whole mg/L of COD at which the plant exports power.
|
|
132
|
+
*
|
|
133
|
+
* Found by scanning upward rather than by inverting the arithmetic, so the
|
|
134
|
+
* answer is the same quantity `balanceFor` reports and cannot drift from it.
|
|
135
|
+
*/
|
|
136
|
+
export function breakEvenCod(): number {
|
|
137
|
+
for (let cod = 0; cod <= 100000; cod++) {
|
|
138
|
+
if (balanceFor(cod).selfPowering) return cod
|
|
139
|
+
}
|
|
140
|
+
throw new Error('no break-even COD below 100 000 mg/L')
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Typical loads, for locating a real stream against the threshold. */
|
|
144
|
+
export const TYPICAL_LOADS: readonly { readonly name: string; readonly cod: number }[] = [
|
|
145
|
+
{ name: 'municipal sewage, dilute', cod: 300 },
|
|
146
|
+
{ name: 'municipal sewage, strong', cod: 800 },
|
|
147
|
+
{ name: 'brewery effluent', cod: 3000 },
|
|
148
|
+
{ name: 'dairy processing', cod: 5000 },
|
|
149
|
+
{ name: 'manure slurry', cod: 40000 },
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
// ============================================================================
|
|
153
|
+
// SELF-CHECK
|
|
154
|
+
// ============================================================================
|
|
155
|
+
|
|
156
|
+
/** Facts this module must satisfy. Returns failures. */
|
|
157
|
+
export function selfTest(): string[] {
|
|
158
|
+
const fail: string[] = []
|
|
159
|
+
|
|
160
|
+
// Clean water carries nothing. This is the point: H2O is not the fuel.
|
|
161
|
+
const clean = balanceFor(0)
|
|
162
|
+
if (clean.energyInLoad !== 0) fail.push('clean water reports non-zero energy')
|
|
163
|
+
if (clean.electricityGenerated !== 0) fail.push('clean water generates electricity')
|
|
164
|
+
if (clean.selfPowering) fail.push('clean water reports as self-powering')
|
|
165
|
+
if (clean.netElectricity !== -TREATMENT_DEMAND_DECIJOULES_PER_LITRE) {
|
|
166
|
+
fail.push('clean water does not owe exactly the treatment demand')
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// A threshold exists, is positive, and is unique — monotone in the load.
|
|
170
|
+
const t = breakEvenCod()
|
|
171
|
+
if (!(t > 0)) fail.push(`break-even COD is ${t}, expected positive`)
|
|
172
|
+
if (balanceFor(t).netElectricity <= 0) fail.push('break-even point does not export')
|
|
173
|
+
if (balanceFor(t - 1).netElectricity > 0) fail.push('a lower COD also exports — not a threshold')
|
|
174
|
+
|
|
175
|
+
// Monotone: more load never yields less electricity.
|
|
176
|
+
let previous = -1
|
|
177
|
+
for (let cod = 0; cod <= 20000; cod += 100) {
|
|
178
|
+
const e = balanceFor(cod).electricityGenerated
|
|
179
|
+
if (e < previous) { fail.push(`electricity fell between loads at COD ${cod}`); break }
|
|
180
|
+
previous = e
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Municipal sewage must sit below the threshold and strong effluent above,
|
|
184
|
+
// or the model has lost contact with the streams it claims to describe.
|
|
185
|
+
const municipal = TYPICAL_LOADS.find((l) => l.name === 'municipal sewage, strong')
|
|
186
|
+
const dairy = TYPICAL_LOADS.find((l) => l.name === 'dairy processing')
|
|
187
|
+
if (municipal && balanceFor(municipal.cod).selfPowering) {
|
|
188
|
+
fail.push('strong municipal sewage reports as self-powering')
|
|
189
|
+
}
|
|
190
|
+
if (dairy && !balanceFor(dairy.cod).selfPowering) {
|
|
191
|
+
fail.push('dairy effluent does not report as self-powering')
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// No arrangement recovers more than the load contains: capture and engine
|
|
195
|
+
// efficiency are both below 100%, so electricity < energy present, always.
|
|
196
|
+
for (const { cod } of TYPICAL_LOADS) {
|
|
197
|
+
const b = balanceFor(cod)
|
|
198
|
+
if (cod > 0 && b.electricityGenerated >= b.energyInLoad) {
|
|
199
|
+
fail.push(`COD ${cod} yields electricity at or above the energy present`)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return fail
|
|
204
|
+
}
|
package/src/vbm-math.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Wave 10: digitalRoot bridges to kernel legacy adapter (0→0).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { pathToFileURL } from 'node:url'
|
|
8
9
|
import { abs, pow } from './0/algebra.ts'
|
|
9
10
|
import { legacyDigitalRoot } from './0/3/6/9/1/2/4/8/7/5/1/a432.roots.ts'
|
|
10
11
|
|
|
@@ -374,6 +375,19 @@ export class VBMNumberAnalyzer {
|
|
|
374
375
|
}
|
|
375
376
|
|
|
376
377
|
// Run examples if this file is executed directly
|
|
377
|
-
|
|
378
|
+
/**
|
|
379
|
+
* True when this file is the entry point Node was started with.
|
|
380
|
+
*
|
|
381
|
+
* ESM has no `require.main`. The CommonJS idiom did not merely fail to detect
|
|
382
|
+
* direct execution here — `require` is undefined in an ES module, so the guard
|
|
383
|
+
* THREW on import and made the whole module unloadable. Nobody importing this
|
|
384
|
+
* ever got far enough to notice the guard was wrong.
|
|
385
|
+
*/
|
|
386
|
+
function isMainModule(): boolean {
|
|
387
|
+
const entry = process.argv[1]
|
|
388
|
+
return entry !== undefined && import.meta.url === pathToFileURL(entry).href
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (isMainModule()) {
|
|
378
392
|
VBMExamples.runAllExamples();
|
|
379
393
|
}
|