← 返回数学手册

scripts/check-math.mjs

与本次数学页面一起发布的源码快照。SHA-256: 0daab81bbad25ecafee952219b278c00f290c496b046a6a88fb52671a534a968

下载原始文件 · 版本指纹清单

1import assert from 'node:assert/strict';
2import { readFile, writeFile, mkdir } from 'node:fs/promises';
3import { sections } from './math/content.mjs';
4
5// Independent numerical checks of the mathematical identities, not a GPU test.
6const results = [];
7const flat = x => Array.isArray(x) ? x.flatMap(flat) : [x];
8function close(label, actual, expected, tolerance = 1e-7) {
9  const a = flat(actual), e = flat(expected);
10  assert.equal(a.length, e.length, label);
11  const error = Math.max(...a.map((v, i) => Math.abs(v - e[i])));
12  assert.ok(Number.isFinite(error) && error < tolerance, `${label}: ${error}`);
13  results.push({ label, maxAbsoluteError: error, tolerance });
14}
15const add = (a, b) => a.map((v, i) => v + b[i]);
16const scale = (a, s) => a.map(v => v * s);
17const dot = (a, b) => a.reduce((s, v, i) => s + v * b[i], 0);
18const tr = a => a[0].map((_, j) => a.map(row => row[j]));
19const mul = (a, b) => a.map(row => tr(b).map(col => dot(row, col)));
20const mv = (a, x) => a.map(row => dot(row, x));
21const outer = (a, b) => a.map(x => b.map(y => x * y));
22const eye = n => Array.from({ length: n }, (_, i) => Array.from({ length: n }, (_, j) => +(i === j)));
23const matrixSubtract = (a, b) => a.map((row, i) => row.map((v, j) => v - b[i][j]));
24function inverse(a) {
25  const n = a.length, augmented = a.map((row, i) => [...row, ...eye(n)[i]]);
26  for (let k = 0; k < n; k++) {
27    let pivot = k;
28    for (let r = k + 1; r < n; r++) if (Math.abs(augmented[r][k]) > Math.abs(augmented[pivot][k])) pivot = r;
29    [augmented[k], augmented[pivot]] = [augmented[pivot], augmented[k]];
30    const diagonal = augmented[k][k];
31    assert.ok(Math.abs(diagonal) > 1e-12);
32    augmented[k] = augmented[k].map(v => v / diagonal);
33    for (let r = 0; r < n; r++) if (r !== k) {
34      const factor = augmented[r][k];
35      augmented[r] = augmented[r].map((v, j) => v - factor * augmented[k][j]);
36    }
37  }
38  return augmented.map(row => row.slice(n));
39}
40function finiteJacobian(fn, x, step) {
41  return tr(x.map((_, k) => {
42    const a = [...x], b = [...x]; a[k] += step; b[k] -= step;
43    return fn(a).map((v, j) => (v - fn(b)[j]) / (2 * step));
44  }));
45}
46function finiteCheck(label, fn, x, expected) {
47  for (const step of [1e-4, 1e-5, 1e-6]) close(`${label} h=${step}`, finiteJacobian(fn, x, step), expected, 2e-6);
48}
49
50const projection = ([x, y, z]) => [100 * x / z, 100 * y / z];
51const J = [[10, 0, -1], [0, 10, -2]];
52finiteCheck('pinhole Jacobian', projection, [1, 2, 10], J);
53const covariance = mul(mul(J, [[1, 0, 0], [0, 4, 0], [0, 0, 9]]), tr(J));
54close('worked projected covariance', covariance.map((row, i) => row.map((v, j) => v + (i === j ? 0.3 : 0))), [[109.3, 18], [18, 436.3]]);
55
56const A = [[2, 0, 0], [0, 1, 0], [0, 0, 1]], b = [1, 0, 0], c = 1;
57const conditional = matrixSubtract(A, outer(b, b));
58close('Schur complement', conditional, eye(3));
59const sigma4 = [...A.map((row, i) => [...row, b[i]]), [...b, c]];
60const z = [0.9, -0.2, 0.7, 0.4], d = add(z.slice(0, 3), scale(b, -z[3] / c));
61close('joint versus conditional quadratic form', dot(z, mv(inverse(sigma4), z)), dot(d, mv(inverse(conditional), d)) + z[3] ** 2 / c);
62
63const C = [[1.4, 0.3], [0.3, 0.8]], Q = inverse(C), pixel = [0.7, -0.2], mean = [0.2, 0.1], beta = 0.4;
64const sigmoid = x => 1 / (1 + Math.exp(-x));
65const alpha = (mu, cov, logit) => {
66  const delta = add(pixel, scale(mu, -1));
67  return sigmoid(logit) * Math.exp(-dot(delta, mv(inverse(cov), delta)) / 2);
68};
69const currentAlpha = alpha(mean, C, beta), qd = mv(Q, add(pixel, scale(mean, -1)));
70finiteCheck('projected mean gradient', mu => [alpha(mu, C, beta)], mean, [scale(qd, currentAlpha)]);
71finiteCheck('independent symmetric covariance entries', ([a, b, c]) => [alpha(mean, [[a, b], [b, c]], beta)], [1.4, 0.3, 0.8], [[currentAlpha / 2 * qd[0] ** 2, currentAlpha * qd[0] * qd[1], currentAlpha / 2 * qd[1] ** 2]]);
72finiteCheck('opacity logit gradient', ([logit]) => [alpha(mean, C, logit)], [beta], [[currentAlpha * (1 - sigmoid(beta))]]);
73
74const colors = [[1, 0, 0], [0, 0, 1], [0.2, 0.7, 0.1]], background = [0.13, 0.23, 0.31], alphas = [0.2, 0.4, 0.3];
75const composite = (as, cs, bg) => as.reduceRight((tail, a, index) => add(scale(cs[index], a), scale(tail, 1 - a)), bg);
76close('ordered two layer example', composite([0.5, 0.5], colors.slice(0, 2), [0, 0, 0]), [0.5, 0, 0.25]);
77const alphaGrad = tr(alphas.map((_, index) => {
78  const T = alphas.slice(0, index).reduce((value, a) => value * (1 - a), 1);
79  const tail = composite(alphas.slice(index + 1), colors.slice(index + 1), background);
80  return scale(add(colors[index], scale(tail, -1)), T);
81}));
82finiteCheck('compositing alpha gradient including background', as => composite(as, colors, background), alphas, alphaGrad);
83
84const time = 0.61, center = 0.43, logWidth = Math.log(0.3), delta = time - center;
85const weight = (t, ell) => Math.exp(-(((t - center) / Math.exp(ell)) ** 2));
86const w = weight(time, logWidth), rho2 = Math.exp(logWidth) ** 2;
87finiteCheck('STG temporal derivative and log-width derivative', ([t, ell]) => [weight(t, ell)], [time, logWidth], [[-2 * delta / rho2 * w, 2 * delta ** 2 / rho2 * w]]);
88const normalize = u => scale(u, 1 / Math.hypot(...u));
89const u = [1.1, 0.2, -0.3, 0.4], q = normalize(u), normalJac = matrixSubtract(eye(4), outer(q, q)).map(row => scale(row, 1 / Math.hypot(...u)));
90finiteCheck('quaternion normalization Jacobian', normalize, u, normalJac);
91
92const html = await readFile(new URL('../public/math/index.html', import.meta.url), 'utf8');
93const ids = [...html.matchAll(/\bid="([^"]+)"/g)].map(match => match[1]);
94assert.equal(new Set(ids).size, ids.length, 'Unique HTML ids');
95for (const [, anchor] of html.matchAll(/href="#([^"]+)"/g)) assert.ok(ids.includes(anchor), 'Missing anchor: ' + anchor);
96const equations = sections.flatMap(section => section.blocks.filter(block => typeof block === 'object'));
97assert.equal(equations.length, 39);
98assert.equal([...html.matchAll(/class="copy-equation"/g)].length, equations.length);
99const saved = JSON.parse(await readFile(new URL('../public/math/equations.json', import.meta.url), 'utf8'));
100assert.deepEqual(saved, equations.map(({ id, title, latex }) => ({ id, title, latex })));
101assert.ok(!/<(?:script|link)[^>]+(?:src|href)="https?:/i.test(html), 'Math page must not require a CDN');
102const report = { status: 'pass', equations: equations.length, numericChecks: results, visualReview: 'Not performed by this numerical and structural check.' };
103await mkdir(new URL('../artifacts/', import.meta.url), { recursive: true });
104await writeFile(new URL('../artifacts/math-checks.json', import.meta.url), JSON.stringify(report, null, 2) + '\n');
105console.log(`PASS: ${results.length} numerical checks, 39 equation sources, all local anchors. Browser visual review not run.`);
106