zeropoint-node 1.0.4 → 1.0.6
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 +176 -0
- package/CITATION.cff +1 -1
- package/README.md +1 -1
- package/dist/legacy-BEa7tljT.js.map +1 -1
- package/dist/legacy-CV8oTnKM.cjs.map +1 -1
- package/package.json +12 -3
- 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.vbm.path.ts +6 -2
- package/src/0/index.ts +16 -0
- package/src/crypto/ml-kem-768-acvp.json +492 -0
- package/src/crypto/ml-kem.test.ts +140 -0
- package/src/crypto/ml-kem.ts +554 -0
- package/src/quantum/error-correction.ts +69 -16
- package/src/quantum/hybrid.ts +2 -2
- package/src/quantum/simulator.ts +19 -0
- package/src/quantum/superposition-execution.ts +9 -4
- package/src/quantum/tomography.ts +4 -4
- package/src/thermo/free-energy.ts +279 -0
- package/src/thermo/wastewater-energy.ts +204 -0
- package/src/verification/lean-bridge.test.ts +109 -228
- package/src/verification/lean-bridge.ts +748 -153
- package/src/crypto/kyber-real.test.ts +0 -198
- package/src/crypto/kyber-real.ts +0 -489
|
@@ -9,14 +9,45 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { floor, abs, max, min, round } from '../0/algebra.ts'
|
|
12
|
-
import { type Register, zeroState, applyGate1, cnot, H, X, Z, probabilities, measureQubit } from './simulator.ts'
|
|
12
|
+
import { type Register, zeroState, applyGate1, cnot, H, X, Z, probabilities, measureQubit, unitOf } from './simulator.ts'
|
|
13
13
|
|
|
14
14
|
export interface StabilizerCode {
|
|
15
15
|
readonly name: string
|
|
16
16
|
readonly logicalQubits: number // Number of encoded logical qubits
|
|
17
17
|
readonly physicalQubits: number // Number of physical qubits
|
|
18
18
|
readonly distance: number // Minimum weight of logical operator
|
|
19
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Stabiliser generators in SYMPLECTIC form: each row is 2n bits,
|
|
21
|
+
* [x_0..x_{n-1} | z_0..z_{n-1}].
|
|
22
|
+
*
|
|
23
|
+
* The previous n-bit form could not express a CSS code at all. An X-type and
|
|
24
|
+
* a Z-type generator over the same support are different operators but
|
|
25
|
+
* identical bit patterns, so the Steane code's six generators collapsed to
|
|
26
|
+
* three distinct rows — and the list that was there had seven rows, of which
|
|
27
|
+
* 13 of the 21 pairs anticommuted. A stabiliser group needs its generators to
|
|
28
|
+
* commute pairwise; that one was not a group.
|
|
29
|
+
*
|
|
30
|
+
* Two rows commute iff their symplectic product is even:
|
|
31
|
+
* <g,h> = x_g . z_h + z_g . x_h (mod 2)
|
|
32
|
+
*/
|
|
33
|
+
readonly generators: readonly (readonly number[])[]
|
|
34
|
+
/** X-type supports, n bits each. */
|
|
35
|
+
readonly xGenerators: readonly (readonly number[])[]
|
|
36
|
+
/** Z-type supports, n bits each. */
|
|
37
|
+
readonly zGenerators: readonly (readonly number[])[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Build a symplectic generator row from separate X and Z supports. */
|
|
41
|
+
function symplectic(x: readonly number[], z: readonly number[]): number[] {
|
|
42
|
+
return [...x, ...z]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Symplectic product mod 2 — zero exactly when the two operators commute. */
|
|
46
|
+
export function symplecticProduct(g: readonly number[], h: readonly number[]): 0 | 1 {
|
|
47
|
+
const n = g.length / 2
|
|
48
|
+
let acc = 0
|
|
49
|
+
for (let i = 0; i < n; i++) acc += g[i]! * h[n + i]! + g[n + i]! * h[i]!
|
|
50
|
+
return (acc % 2) as 0 | 1
|
|
20
51
|
}
|
|
21
52
|
|
|
22
53
|
export interface SyndromeResult {
|
|
@@ -42,28 +73,50 @@ export interface CorrelationValue {
|
|
|
42
73
|
}
|
|
43
74
|
|
|
44
75
|
// Simple 3-qubit repetition code: encode 1 logical qubit in 3 physical qubits
|
|
76
|
+
const REPETITION_Z: number[][] = [
|
|
77
|
+
[1, 1, 0], // Z0 Z1
|
|
78
|
+
[0, 1, 1], // Z1 Z2
|
|
79
|
+
]
|
|
80
|
+
|
|
45
81
|
export const REPETITION_3_CODE: StabilizerCode = {
|
|
46
82
|
name: 'Repetition [3,1,1]',
|
|
47
83
|
logicalQubits: 1,
|
|
48
84
|
physicalQubits: 3,
|
|
85
|
+
// Distance 1 as a QUANTUM code: it detects bit flips and is blind to phase
|
|
86
|
+
// flips, so a single Z is an undetectable logical error.
|
|
49
87
|
distance: 1,
|
|
50
|
-
|
|
88
|
+
xGenerators: [],
|
|
89
|
+
zGenerators: REPETITION_Z,
|
|
90
|
+
generators: REPETITION_Z.map((z) => symplectic([0, 0, 0], z)),
|
|
51
91
|
}
|
|
52
92
|
|
|
53
93
|
// Steane code [7,1,3]: 7 physical qubits, 1 logical, distance 3
|
|
94
|
+
/**
|
|
95
|
+
* The three parity checks of the classical [7,4,3] Hamming code. Steane's code
|
|
96
|
+
* is the CSS construction over them, so the SAME three supports appear once as
|
|
97
|
+
* X-type generators and once as Z-type — six in total, which is n - k = 7 - 1.
|
|
98
|
+
*
|
|
99
|
+
* Each pair overlaps in exactly two positions, so every X row commutes with
|
|
100
|
+
* every Z row.
|
|
101
|
+
*/
|
|
102
|
+
const HAMMING_CHECKS: number[][] = [
|
|
103
|
+
[0, 0, 0, 1, 1, 1, 1], // qubits 3,4,5,6
|
|
104
|
+
[0, 1, 1, 0, 0, 1, 1], // qubits 1,2,5,6
|
|
105
|
+
[1, 0, 1, 0, 1, 0, 1], // qubits 0,2,4,6
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
const ZERO7 = [0, 0, 0, 0, 0, 0, 0]
|
|
109
|
+
|
|
54
110
|
export const STEANE_CODE: StabilizerCode = {
|
|
55
111
|
name: 'Steane [7,1,3]',
|
|
56
112
|
logicalQubits: 1,
|
|
57
113
|
physicalQubits: 7,
|
|
58
114
|
distance: 3,
|
|
115
|
+
xGenerators: HAMMING_CHECKS,
|
|
116
|
+
zGenerators: HAMMING_CHECKS,
|
|
59
117
|
generators: [
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
[0, 1, 1, 0, 0, 1, 0],
|
|
63
|
-
[1, 1, 0, 0, 0, 0, 1],
|
|
64
|
-
[1, 0, 1, 0, 0, 1, 0],
|
|
65
|
-
[0, 1, 1, 0, 1, 0, 0],
|
|
66
|
-
[1, 0, 0, 1, 1, 0, 0],
|
|
118
|
+
...HAMMING_CHECKS.map((x) => symplectic(x, ZERO7)),
|
|
119
|
+
...HAMMING_CHECKS.map((z) => symplectic(ZERO7, z)),
|
|
67
120
|
],
|
|
68
121
|
}
|
|
69
122
|
|
|
@@ -102,15 +155,15 @@ export function measureSyndromeRepetition(
|
|
|
102
155
|
|
|
103
156
|
// Syndrome 0: Z1 Z2 (correlation between qubits 0 and 1)
|
|
104
157
|
// In computational basis, this is even/odd parity of (q0, q1)
|
|
105
|
-
const m0 = measureQubit(reg, 0, s)
|
|
158
|
+
const m0 = measureQubit(reg, 0, unitOf(s))
|
|
106
159
|
s = (1664525 * s + 1013904223) % 4294967296
|
|
107
|
-
const m1 = measureQubit(reg, 1, s)
|
|
160
|
+
const m1 = measureQubit(reg, 1, unitOf(s))
|
|
108
161
|
s = (1664525 * s + 1013904223) % 4294967296
|
|
109
162
|
const parity01 = (m0.bit + m1.bit) % 2
|
|
110
163
|
syndrome.push(parity01 as 0 | 1)
|
|
111
164
|
|
|
112
165
|
// Syndrome 1: Z2 Z3 (correlation between qubits 1 and 2)
|
|
113
|
-
const m2 = measureQubit(reg, 2, s)
|
|
166
|
+
const m2 = measureQubit(reg, 2, unitOf(s))
|
|
114
167
|
const parity12 = (m1.bit + m2.bit) % 2
|
|
115
168
|
syndrome.push(parity12 as 0 | 1)
|
|
116
169
|
|
|
@@ -136,11 +189,11 @@ export function correctRepetition(reg: Register, syndrome: SyndromeResult): Regi
|
|
|
136
189
|
// Decode logical qubit (majority vote for repetition code)
|
|
137
190
|
export function decodeLogicalRepetition(reg: Register, seed: number = 0): 0 | 1 {
|
|
138
191
|
let s = seed
|
|
139
|
-
const m0 = measureQubit(reg, 0, s)
|
|
192
|
+
const m0 = measureQubit(reg, 0, unitOf(s))
|
|
140
193
|
s = (1664525 * s + 1013904223) % 4294967296
|
|
141
|
-
const m1 = measureQubit(reg, 1, s)
|
|
194
|
+
const m1 = measureQubit(reg, 1, unitOf(s))
|
|
142
195
|
s = (1664525 * s + 1013904223) % 4294967296
|
|
143
|
-
const m2 = measureQubit(reg, 2, s)
|
|
196
|
+
const m2 = measureQubit(reg, 2, unitOf(s))
|
|
144
197
|
|
|
145
198
|
// Majority vote
|
|
146
199
|
const vote = m0.bit + m1.bit + m2.bit
|
package/src/quantum/hybrid.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { sqrt, abs, floor, log2, max, min, round } from '../0/algebra.ts'
|
|
12
|
-
import { type Register, zeroState, applyGate1, ry, rz, cnot, measureQubit, probabilities } from './simulator.ts'
|
|
12
|
+
import { type Register, zeroState, applyGate1, ry, rz, cnot, measureQubit, probabilities, unitOf } from './simulator.ts'
|
|
13
13
|
import { type VQEResult, vqeAdaptive } from './variational-optimizer.ts'
|
|
14
14
|
import { AdaptiveOptimizer } from './adaptive.ts'
|
|
15
15
|
|
|
@@ -72,7 +72,7 @@ export function ansatzRotationEntangle(
|
|
|
72
72
|
|
|
73
73
|
// Classify via measurement: measure qubit 0, output 0/1
|
|
74
74
|
export function classifyMeasurement(reg: Register, seed: number = 0): ClassificationResult {
|
|
75
|
-
const meas = measureQubit(reg, 0, seed)
|
|
75
|
+
const meas = measureQubit(reg, 0, unitOf(seed))
|
|
76
76
|
const probs = probabilities(reg)
|
|
77
77
|
const confidence = max(probs[0]!, probs[1]!)
|
|
78
78
|
const circuitDepth = 0 // Placeholder; actual depth from circuit analysis
|
package/src/quantum/simulator.ts
CHANGED
|
@@ -182,6 +182,25 @@ export function measure(reg: Register, unit: number): { outcome: number; collaps
|
|
|
182
182
|
* renormalised state. On an entangled state this is where the correlation
|
|
183
183
|
* bites — measuring one qubit of a Bell pair fixes the other.
|
|
184
184
|
*/
|
|
185
|
+
/**
|
|
186
|
+
* The modulus of the LCG these modules use for deterministic sampling.
|
|
187
|
+
*/
|
|
188
|
+
export const RNG_MODULUS = 4294967296
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Map an LCG state onto the unit interval `measureQubit` expects.
|
|
192
|
+
*
|
|
193
|
+
* `measureQubit` takes a UNIT in [0, 1), not a seed. Four call sites passed the
|
|
194
|
+
* raw LCG state instead, which is an integer up to 2^32. Since the outcome test
|
|
195
|
+
* is `unit < 1 - pOne`, any state of 1 or more forced the outcome to 1 — so
|
|
196
|
+
* `measureZ` on |0> returned 999 ones in 1000 shots, and repetition-code
|
|
197
|
+
* syndrome extraction reported a clean codeword as an error.
|
|
198
|
+
*/
|
|
199
|
+
export function unitOf(seed: number): number {
|
|
200
|
+
const m = seed % RNG_MODULUS
|
|
201
|
+
return (m < 0 ? m + RNG_MODULUS : m) / RNG_MODULUS
|
|
202
|
+
}
|
|
203
|
+
|
|
185
204
|
export function measureQubit(reg: Register, q: number, unit: number): { bit: 0 | 1; collapsed: Register } {
|
|
186
205
|
const bit = 1 << q
|
|
187
206
|
let pOne = 0
|
|
@@ -351,9 +351,14 @@ MEASUREMENT & COLLAPSE:
|
|
|
351
351
|
- Run all tests simultaneously
|
|
352
352
|
- Theater share ${round(THEATER_SHARE * AMPLITUDE_SCALE)}/${AMPLITUDE_SCALE}, reality amplified
|
|
353
353
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
354
|
+
${
|
|
355
|
+
execution.system_correctness === 'collapsed_to_valid'
|
|
356
|
+
? `COLLAPSED.
|
|
357
|
+
The measurement cleared the threshold, so the phases resolved together.`
|
|
358
|
+
: `STILL SUPERPOSED — NOT ALL AT ONCE.
|
|
359
|
+
Interference reached ${round(interference.working_solution_probability * AMPLITUDE_SCALE)}/${AMPLITUDE_SCALE}, under the ${round(COLLAPSE_THRESHOLD * AMPLITUDE_SCALE)}/${AMPLITUDE_SCALE} a collapse needs.
|
|
360
|
+
While anything stays open the sequence does not compute all at once, and this
|
|
361
|
+
model must not claim it did. ${execution.next_action}`
|
|
362
|
+
}
|
|
358
363
|
`
|
|
359
364
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { sqrt, abs, floor, log2 } from '../0/algebra.ts'
|
|
8
|
-
import { type Register, zeroState, applyGate1, H, S, cx, measureQubit, probabilities, cabs2 } from './simulator.ts'
|
|
8
|
+
import { type Register, zeroState, applyGate1, H, S, cx, measureQubit, probabilities, cabs2, unitOf } from './simulator.ts'
|
|
9
9
|
|
|
10
10
|
interface Complex {
|
|
11
11
|
readonly re: number
|
|
@@ -44,7 +44,7 @@ export function measureZ(reg: Register, qubit: number, shots: number = 1000, see
|
|
|
44
44
|
const counts = [0, 0]
|
|
45
45
|
let s = seed
|
|
46
46
|
for (let i = 0; i < shots; i++) {
|
|
47
|
-
const meas = measureQubit(reg, qubit, s)
|
|
47
|
+
const meas = measureQubit(reg, qubit, unitOf(s))
|
|
48
48
|
counts[meas.bit]++
|
|
49
49
|
s = (1664525 * s + 1013904223) % 4294967296
|
|
50
50
|
}
|
|
@@ -58,7 +58,7 @@ export function measureX(reg: Register, qubit: number, shots: number = 1000, see
|
|
|
58
58
|
const counts = [0, 0]
|
|
59
59
|
let s = seed
|
|
60
60
|
for (let i = 0; i < shots; i++) {
|
|
61
|
-
const meas = measureQubit(rotated, qubit, s)
|
|
61
|
+
const meas = measureQubit(rotated, qubit, unitOf(s))
|
|
62
62
|
counts[meas.bit]++
|
|
63
63
|
s = (1664525 * s + 1013904223) % 4294967296
|
|
64
64
|
}
|
|
@@ -79,7 +79,7 @@ export function measureY(reg: Register, qubit: number, shots: number = 1000, see
|
|
|
79
79
|
const counts = [0, 0]
|
|
80
80
|
let s = seed
|
|
81
81
|
for (let i = 0; i < shots; i++) {
|
|
82
|
-
const meas = measureQubit(s2, qubit, s)
|
|
82
|
+
const meas = measureQubit(s2, qubit, unitOf(s))
|
|
83
83
|
counts[meas.bit]++
|
|
84
84
|
s = (1664525 * s + 1013904223) % 4294967296
|
|
85
85
|
}
|
|
@@ -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
|
+
}
|