Download quant.js from AEUPH/DMCRA-LAB: direct link, hf CLI and curl.
- Browser
- Download file 21.9 kB
-
https://huggingface.co/spaces/AEUPH/DMCRA-LAB/resolve/main/quant.js
- Command line
-
hf download hf://spaces/AEUPH/DMCRA-LAB/quant.js
-
curl -L -o quant.js https://huggingface.co/spaces/AEUPH/DMCRA-LAB/resolve/main/quant.js
21.9 kB
| /* quant.js — the DMCRA level-set quantiser: a codebook chosen per block, from an elastic family. | |
| * | |
| * WHAT THIS IS, AND WHAT IT IS NOT | |
| * The DMCRA document proposes replacing fixed ternary `{-1,0,+1}` with an elastic family of grids | |
| * that "unclamps" as pressure demands. Stripped of the hardware vocabulary that a static page | |
| * cannot implement, that is one concrete, decidable mechanism: a weight matrix is cut into blocks, | |
| * each block is assigned a LEVEL SET from a nested family, and the assignment is chosen by a | |
| * measured criterion under a bit budget. This file is that mechanism and nothing else. There is no | |
| * routing fabric here, and no zero-bus: `0/0/0` is three zeros and this module reports, by exact | |
| * arithmetic, that it reconstructs identically to `{0}`. | |
| * | |
| * Two scale rules, on purpose: | |
| * rule "meanabs" — scale = mean(|w|) per block, the convention this repo's weights.js already | |
| * uses for ternary. Reproduced here so a test can assert that this quantiser's | |
| * ternary is IDENTICAL to the already-verified `toTernary`/`fromTernary`. That | |
| * assertion is what stops this file from drifting away from the project. | |
| * rule "lsq" — least-squares optimal step per block (the classic LSQ iteration, Esser et al. | |
| * 2020): s <- sum(w*q)/sum(q*q) with q = nearest level of w/s. Default for the | |
| * level-set ladder, because it gives EVERY grid its own optimal scale, so the | |
| * ladder compares codebooks rather than comparing one grid's scale artefact | |
| * against another's. | |
| * | |
| * Bit accounting is explicit and separated, because the interesting honest wrinkle is that they | |
| * disagree: `indexBits` is what an encoder must carry per weight, `effBits` adds the per-block scale | |
| * overhead (which is what makes small blocks expensive), and `entropyBits` is the measured Shannon | |
| * entropy of the level histogram. A grid that claims five channels but decodes to three distinct | |
| * values is reported as exactly that: `distinctLevels` < `levels.length`, `wastedChannels` > 0. | |
| */ | |
| (function (root, factory) { | |
| var Q = factory(); | |
| if (typeof module !== "undefined" && module.exports) module.exports = Q; | |
| root.DMCRAQUANT = Q; | |
| })(typeof globalThis !== "undefined" ? globalThis : this, function () { | |
| "use strict"; | |
| /* ---------------------------------------------------------------- the elastic family | |
| * The document's family is nested: {+-1} -> {0,+-1} -> {0,+-1,+-2} -> ... . Two extra grids are | |
| * included because they are what an equal-bits contest actually needs: `quad` spends the SAME two | |
| * index bits as ternary but on two magnitudes instead of a zero, and `oct` spends the same three | |
| * bits as penta on four magnitudes instead of a zero. Without those two, "unclamping ternary" | |
| * would only ever be compared against a cheaper code, which would decide nothing. | |
| * `ternaryDup` is not a proposal; it is the document's `0/0/0` written down so it can be measured. | |
| */ | |
| var GRIDS = { | |
| binary: { levels: [0, 1], note: "sign only, 1 index bit" }, | |
| binaryPN: { levels: [-1, 1], note: "bipolar sign only, 1 index bit" }, | |
| ternary: { levels: [-1, 0, 1], note: "BitNet b1.58, 2 index bits" }, | |
| quad: { levels: [-2, -1, 1, 2], note: "2 magnitudes, no zero, 2 index bits" }, | |
| ternaryDup:{ levels: [-1, 0, 0, 0, 1], note: "the document's {0/0/0}: 5 channels, 3 distinct values" }, | |
| penta: { levels: [-2, -1, 0, 1, 2], note: "3 index bits" }, | |
| hepta: { levels: [-3, -2, -1, 0, 1, 2, 3], note: "3 index bits, same cost as penta" }, | |
| oct: { levels: [-4, -3, -2, -1, 1, 2, 3, 4], note: "4 magnitudes, no zero, 3 index bits" }, | |
| nona: { levels: [-4, -3, -2, -1, 0, 1, 2, 3, 4], note: "4 index bits" } | |
| }; | |
| var SCALE_BYTES = 4; | |
| function gridOf(grid) { | |
| if (typeof grid === "string") { | |
| if (!GRIDS[grid]) throw new Error("unknown grid: " + grid); | |
| return GRIDS[grid].levels.slice(); | |
| } | |
| if (Object.prototype.toString.call(grid) === "[object Array]") { | |
| if (!grid.length) throw new Error("empty level set"); | |
| return grid.slice(); | |
| } | |
| throw new Error("grid must be a known name or an array of levels"); | |
| } | |
| function indexBitsFor(k) { return Math.max(1, Math.ceil(Math.log2(k))); } | |
| /* Distinct DECODED values. This is what makes `ternaryDup` measurable rather than rhetorical: | |
| * three zero channels decode to one value, so the encoding is wider than the codebook. */ | |
| function distinctValues(levels) { | |
| var seen = Object.create(null), n = 0, i, v; | |
| for (i = 0; i < levels.length; i++) { v = levels[i]; if (seen[v] !== 1) { seen[v] = 1; n++; } } | |
| return n; | |
| } | |
| function gridInfo(grid) { | |
| var levels = gridOf(grid); | |
| var maxLevel = 0, i; | |
| for (i = 0; i < levels.length; i++) if (Math.abs(levels[i]) > maxLevel) maxLevel = Math.abs(levels[i]); | |
| var d = distinctValues(levels); | |
| var idx = indexBitsFor(levels.length); | |
| var dec = indexBitsFor(d); | |
| return { | |
| levels: levels, count: levels.length, maxLevel: maxLevel, | |
| distinctLevels: d, distinctBits: dec, | |
| indexBits: idx, decodeBits: dec, | |
| wastedChannels: levels.length - d, | |
| /* Channels paid for that the decoder cannot distinguish. Zero for every honest grid. */ | |
| wastedBits: idx - dec | |
| }; | |
| } | |
| /* ---------------------------------------------------------------- nearest level */ | |
| function isContiguousIntegers(levels) { | |
| for (var i = 1; i < levels.length; i++) if (levels[i] !== levels[i - 1] + 1) return false; | |
| return true; | |
| } | |
| /* Nearest level to x, given that the levels were built by scaling integers. Ties resolve to the | |
| * first (lowest) candidate, which is why three zero channels can never disagree: every one of | |
| * them is 0, so the reconstructed value is 0 whichever way the tie falls. */ | |
| function nearestIndex(levels, x, contiguous, lo, hi) { | |
| if (contiguous) { | |
| var r = Math.round(x); | |
| if (r < lo) r = lo; else if (r > hi) r = hi; | |
| return r - lo; | |
| } | |
| var best = 0, bestD = Infinity, i, d; | |
| for (i = 0; i < levels.length; i++) { | |
| d = Math.abs(x - levels[i]); | |
| if (d < bestD) { bestD = d; best = i; } | |
| } | |
| return best; | |
| } | |
| /* ---------------------------------------------------------------- block statistics */ | |
| /* Per-block moments. `variance` is the document's stated pressure; it is computed here so the | |
| * allocation experiment can test whether it is the RIGHT pressure, and `mse` per grid is kept so | |
| * the benefit-driven allocator can allocate by measured error reduction instead. */ | |
| function blockStats(values, blockSize, gridNames, rule) { | |
| var n = values.length; | |
| var bs = blockSize > 0 ? blockSize : n; | |
| var out = [], off; | |
| for (off = 0; off < n; off += bs) { | |
| var len = Math.min(bs, n - off); | |
| var sum = 0, absSum = 0, sq = 0, i, v; | |
| for (i = 0; i < len; i++) { v = values[off + i]; sum += v; absSum += Math.abs(v); sq += v * v; } | |
| var mean = sum / len; | |
| var variance = Math.max(0, sq / len - mean * mean); | |
| var rec = { offset: off, len: len, mean: mean, meanAbs: absSum / len, | |
| variance: variance, sd: Math.sqrt(variance), rms: Math.sqrt(sq / len), mse: {} }; | |
| if (gridNames) { | |
| for (i = 0; i < gridNames.length; i++) { | |
| var q = quantiseBlock(values, off, len, gridNames[i], rule); | |
| rec.mse[gridNames[i]] = q.mse; | |
| } | |
| } | |
| out.push(rec); | |
| } | |
| return out; | |
| } | |
| /* ---------------------------------------------------------------- the scale step */ | |
| function meanAbsOf(values, off, len) { | |
| var s = 0, i; | |
| for (i = 0; i < len; i++) s += Math.abs(values[off + i]); | |
| return s / len; | |
| } | |
| /* LSQ step: iterate s <- sum(w*q)/sum(q*q) until it stops moving. This is the same update used in | |
| * learned step-size quantization, run here as a few fixed-point iterations per block. */ | |
| function lsqScale(values, off, len, levels, contiguous, lo, hi, seed) { | |
| var s = seed > 0 ? seed : meanAbsOf(values, off, len); | |
| if (!(s > 0)) { | |
| /* An all-zero block: any scale reconstructs it exactly. */ | |
| return 0; | |
| } | |
| var it, i, num, den, q, w, s2; | |
| for (it = 0; it < 6; it++) { | |
| num = 0; den = 0; | |
| for (i = 0; i < len; i++) { | |
| w = values[off + i]; | |
| q = levels[nearestIndex(levels, w / s, contiguous, lo, hi)]; | |
| num += w * q; den += q * q; | |
| } | |
| if (den <= 0) break; | |
| s2 = num / den; | |
| if (!(s2 > 0)) break; | |
| if (Math.abs(s2 - s) <= 1e-12 * s) { s = s2; break; } | |
| s = s2; | |
| } | |
| return s; | |
| } | |
| /* ---------------------------------------------------------------- one block */ | |
| /* Quantise one block under one grid. Returns the reconstruction, the scale, and both error and | |
| * bit accounts. `rule` is "lsq" (default) or "meanabs" (the weights.js ternary convention). */ | |
| function quantiseBlock(values, off, len, grid, rule) { | |
| var info = gridInfo(grid); | |
| var levels = info.levels; | |
| var contiguous = isContiguousIntegers(levels); | |
| var lo = contiguous ? levels[0] : 0, hi = contiguous ? levels[levels.length - 1] : 0; | |
| var s; | |
| if (rule === "meanabs") s = meanAbsOf(values, off, len); | |
| else s = lsqScale(values, off, len, levels, contiguous, lo, hi, meanAbsOf(values, off, len)); | |
| var rec = new Float32Array(len); | |
| var counts = new Array(levels.length); | |
| for (var c = 0; c < counts.length; c++) counts[c] = 0; | |
| var sse = 0, sae = 0, maxAbs = 0, i, w, qi, r, d; | |
| for (i = 0; i < len; i++) { | |
| w = values[off + i]; | |
| qi = s > 0 ? nearestIndex(levels, w / s, contiguous, lo, hi) : 0; | |
| counts[qi]++; | |
| r = s > 0 ? levels[qi] * s : 0; | |
| rec[i] = r; | |
| d = w - r; sse += d * d; sae += Math.abs(d); | |
| if (Math.abs(d) > maxAbs) maxAbs = Math.abs(d); | |
| } | |
| return { | |
| grid: typeof grid === "string" ? grid : "custom", levels: levels, scale: s, | |
| values: rec, counts: counts, | |
| mse: len ? sse / len : 0, mae: len ? sae / len : 0, maxAbs: maxAbs, | |
| bits: blockBits(len, s, info, counts) | |
| }; | |
| } | |
| /* Bit account for one block. `indexBits` travels per weight; the scale is one 4-byte float per | |
| * block, so its share is 32/len bits per weight — the term that punishes small blocks. */ | |
| function blockBits(len, scale, info, counts) { | |
| var indexBits = info.indexBits; | |
| var scaleBits = len ? (SCALE_BYTES * 8) / len : 0; | |
| var entropy = 0, total = len || 1, j, p; | |
| if (counts) { | |
| for (j = 0; j < counts.length; j++) { | |
| if (!counts[j]) continue; | |
| p = counts[j] / total; | |
| entropy -= p * Math.log2(p); | |
| } | |
| } | |
| return { | |
| indexBits: indexBits, scaleBits: scaleBits, effBits: indexBits + scaleBits, | |
| entropyBits: entropy + (scale ? scaleBits : 0), | |
| decodeBits: info.decodeBits, wastedBits: info.wastedBits, | |
| wastedChannels: info.wastedChannels, distinctLevels: info.distinctLevels, | |
| channels: info.count, scaleZero: !(scale > 0) | |
| }; | |
| } | |
| /* ---------------------------------------------------------------- whole tensor */ | |
| /* Quantise a whole array under one grid. Returns the reconstruction plus an aggregate bit account | |
| * that is the length-weighted mean of the per-block accounts, so a caller can compare two grids | |
| * at equal bits rather than at equal nominal precision. */ | |
| function quantise(values, opts) { | |
| opts = opts || {}; | |
| var grid = opts.grid === undefined ? "ternary" : opts.grid; | |
| var rule = opts.rule === "meanabs" ? "meanabs" : "lsq"; | |
| var bs = opts.blockSize | 0; | |
| var n = values.length; | |
| var size = bs > 0 ? bs : n; | |
| var out = new Float32Array(n); | |
| var blocks = [], off, acc = null; | |
| for (off = 0; off < n; off += size) { | |
| var len = Math.min(size, n - off); | |
| var q = quantiseBlock(values, off, len, grid, rule); | |
| out.set(q.values, off); | |
| blocks.push({ offset: off, len: len, scale: q.scale, mse: q.mse, bits: q.bits, | |
| counts: q.counts, levels: q.levels }); | |
| acc = acc ? acc : { | |
| indexBits: 0, scaleBits: 0, effBits: 0, entropyBits: 0, decodeBits: 0, | |
| wastedBits: 0, wastedChannels: q.bits.wastedChannels, distinctLevels: q.bits.distinctLevels, | |
| channels: q.bits.channels, grid: q.grid, name: typeof grid === "string" ? grid : "custom" | |
| }; | |
| acc.indexBits += q.bits.indexBits * len / n; | |
| acc.scaleBits += q.bits.scaleBits * len / n; | |
| acc.effBits += q.bits.effBits * len / n; | |
| acc.entropyBits += q.bits.entropyBits * len / n; | |
| acc.decodeBits += q.bits.decodeBits * len / n; | |
| acc.wastedBits += q.bits.wastedBits * len / n; | |
| } | |
| if (!acc) { | |
| acc = { indexBits: 0, scaleBits: 0, effBits: 0, entropyBits: 0, decodeBits: 0, | |
| wastedBits: 0, wastedChannels: 0, distinctLevels: 0, channels: 0, | |
| grid: String(grid), name: typeof grid === "string" ? grid : "custom" }; | |
| } | |
| acc.blockSize = bs; acc.rule = rule; acc.count = n; | |
| acc.storedBytes = Math.ceil(n * acc.indexBits / 8) + blocks.length * SCALE_BYTES; | |
| acc.floatBytes = n * 4; | |
| acc.ratio = acc.storedBytes ? acc.floatBytes / acc.storedBytes : 0; | |
| var err = measure(values, out); | |
| acc.mse = err.mse; acc.rmse = err.rmse; acc.mae = err.mae; acc.maxAbs = err.maxAbs; | |
| return { values: out, blocks: blocks, bits: acc, error: err }; | |
| } | |
| /* ---------------------------------------------------------------- error */ | |
| function measure(orig, rec) { | |
| var n = Math.min(orig.length, rec.length), i, d, sse = 0, sae = 0, maxAbs = 0, e0 = 0; | |
| for (i = 0; i < n; i++) { | |
| d = orig[i] - rec[i]; | |
| sse += d * d; sae += Math.abs(d); | |
| if (Math.abs(d) > maxAbs) maxAbs = Math.abs(d); | |
| e0 += orig[i] * orig[i]; | |
| } | |
| return { | |
| count: n, mse: n ? sse / n : 0, rmse: n ? Math.sqrt(sse / n) : 0, | |
| mae: n ? sae / n : 0, maxAbs: maxAbs, | |
| /* Fraction of the original energy the reconstruction retains; 1 means nothing was lost. */ | |
| energyRetained: e0 > 0 ? 1 - sse / e0 : 1 | |
| }; | |
| } | |
| /* Exact equality of two reconstructions, used by the {0/0/0} identity test. Compares bit patterns | |
| * rather than a tolerance, because the claim under test is identity, not closeness. */ | |
| function identical(a, b) { | |
| if (a.length !== b.length) return false; | |
| for (var i = 0; i < a.length; i++) if (a[i] !== b[i]) return false; | |
| return true; | |
| } | |
| /* ---------------------------------------------------------------- elastic allocation */ | |
| /* The allocator. This is the DMCRA mechanism made decidable: given a bit budget, assign each block | |
| * a grid so that measured reconstruction error is as low as possible. | |
| * | |
| * key "benefit" (default) — greedy by MEASURED error reduction per extra bit, using each | |
| * block's per-grid MSE. The "pressure" is benefit/cost, not variance. | |
| * key "variance" — the document's stated rule: rank blocks by variance and unclamp the most | |
| * dispersed ones first, ignoring how much each block's error would actually | |
| * improve. Implemented faithfully so it can be compared, not assumed inferior. | |
| * | |
| * Budget is an average of `indexBits` per weight. The greedy walks upgrades from the cheapest | |
| * candidate upward and stops when the next cheapest upgrade no longer fits, so the realised | |
| * average is <= budget and, when the budget happens to be reachable exactly, equals it. | |
| */ | |
| function allocate(values, opts) { | |
| opts = opts || {}; | |
| var candidates = (opts.candidates || ["binary", "ternary", "penta", "hepta"]).slice(); | |
| var rule = opts.rule === "meanabs" ? "meanabs" : "lsq"; | |
| var budget = opts.budget === undefined ? 2 : opts.budget; | |
| var key = opts.key === "variance" ? "variance" : "benefit"; | |
| var bs = opts.blockSize | 0; | |
| var n = values.length; | |
| var size = bs > 0 ? bs : n; | |
| candidates.sort(function (a, b) { return gridInfo(a).indexBits - gridInfo(b).indexBits; }); | |
| var stats = blockStats(values, size, candidates, rule); | |
| var nBlocks = stats.length; | |
| if (!nBlocks) { | |
| return { grids: [], stats: [], avgBits: 0, bits: null, values: new Float32Array(0), | |
| error: measure(values, new Float32Array(0)), key: key, candidates: candidates }; | |
| } | |
| /* Costs, and the exact error reduction each single-step upgrade would buy. */ | |
| var cost = candidates.map(function (c) { return gridInfo(c).indexBits; }); | |
| var level = stats.map(function () { return 0; }); // index into candidates | |
| var totalWeight = 0; | |
| stats.forEach(function (s) { totalWeight += s.len; }); | |
| function currentBits() { | |
| var b = 0; | |
| for (var i = 0; i < nBlocks; i++) b += cost[level[i]] * stats[i].len / totalWeight; | |
| return b; | |
| } | |
| /* A plain greedy on single-step upgrades. Unclamping is monotone: a block only moves to a | |
| * costlier grid, and only when that grid actually reduces its measured error, so a grid that | |
| * costs more without helping is never selected. Each pass takes the single best-scoring upgrade | |
| * that still fits the budget, which is the standard greedy for this separable problem. The two | |
| * keys differ only in the score: benefit uses measured error reduction per extra bit, variance | |
| * uses the block's own dispersion, which is the rule the document names. */ | |
| var guard = 0; | |
| while (guard++ < nBlocks * candidates.length + 10) { | |
| var best = -1, bestScore = -Infinity, i, next, gain, dCost, score; | |
| for (i = 0; i < nBlocks; i++) { | |
| next = level[i] + 1; | |
| if (next >= candidates.length) continue; | |
| gain = (stats[i].mse[candidates[level[i]]] - stats[i].mse[candidates[next]]) * stats[i].len; | |
| if (gain <= 0) continue; | |
| dCost = (cost[next] - cost[level[i]]) * stats[i].len / totalWeight; | |
| if (dCost > 0 && currentBits() + dCost > budget + 1e-9) continue; | |
| if (key === "variance") score = stats[i].variance + gain * 1e-18; | |
| else score = dCost > 0 ? gain / dCost : gain; | |
| if (score > bestScore) { bestScore = score; best = i; } | |
| } | |
| if (best < 0) break; | |
| level[best]++; | |
| } | |
| /* Realise the assignment. */ | |
| var out = new Float32Array(n); | |
| var grids = [], off = 0, i2; | |
| var accBits = { indexBits: 0, scaleBits: 0, effBits: 0, entropyBits: 0, storedBytes: 0 }; | |
| for (i2 = 0; i2 < nBlocks; i2++) { | |
| var st = stats[i2]; | |
| var name = candidates[level[i2]]; | |
| var q = quantiseBlock(values, st.offset, st.len, name, rule); | |
| out.set(q.values, st.offset); | |
| grids.push(name); | |
| accBits.indexBits += q.bits.indexBits * st.len / n; | |
| accBits.scaleBits += q.bits.scaleBits * st.len / n; | |
| accBits.effBits += q.bits.effBits * st.len / n; | |
| accBits.entropyBits += q.bits.entropyBits * st.len / n; | |
| accBits.storedBytes += Math.ceil(st.len * q.bits.indexBits / 8) + SCALE_BYTES; | |
| } | |
| accBits.count = n; accBits.blockSize = bs; accBits.rule = rule; | |
| accBits.mode = "elastic"; accBits.key = key; accBits.budget = budget; | |
| accBits.storedBytes = Math.ceil(accBits.storedBytes); | |
| accBits.floatBytes = n * 4; | |
| accBits.ratio = accBits.storedBytes ? accBits.floatBytes / accBits.storedBytes : 0; | |
| var err2 = measure(values, out); | |
| accBits.mse = err2.mse; accBits.rmse = err2.rmse; accBits.mae = err2.mae; | |
| accBits.maxAbs = err2.maxAbs; | |
| var hist = Object.create(null); | |
| grids.forEach(function (g) { hist[g] = (hist[g] || 0) + 1; }); | |
| accBits.gridHistogram = hist; | |
| return { values: out, grids: grids, stats: stats, bits: accBits, error: err2, | |
| avgBits: accBits.indexBits, key: key, candidates: candidates, | |
| /* The ladder the allocator was allowed to climb, for reporting. */ | |
| costs: candidates.map(function (c) { return { grid: c, indexBits: gridInfo(c).indexBits }; }) }; | |
| } | |
| /* ---------------------------------------------------------------- the {0/0/0} experiment */ | |
| /* Everything needed to settle the document's multi-channel-centroid claim numerically: the same | |
| * weights quantised with one zero channel and with three, the reconstruction compared for exact | |
| * inequality, and the channel accounting of each. A three-channel zero that decodes identically | |
| * is not an information channel; the caller can print both rows and see that. */ | |
| function zeroChannelTest(values, opts) { | |
| opts = opts || {}; | |
| var bs = opts.blockSize | 0; | |
| var one = quantise(values, { grid: "ternary", blockSize: bs, rule: opts.rule }); | |
| var three = quantise(values, { grid: "ternaryDup", blockSize: bs, rule: opts.rule }); | |
| return { | |
| one: one, three: three, | |
| identical: identical(one.values, three.values), | |
| info: { ternary: gridInfo("ternary"), ternaryDup: gridInfo("ternaryDup") } | |
| }; | |
| } | |
| /* ---------------------------------------------------------------- HDC-side helpers */ | |
| /* Shannon entropy in bits of a categorical histogram, used by the bundling experiment to compare | |
| * multi-channel codes at equal information content rather than equal channel count. */ | |
| function entropy(counts) { | |
| var total = 0, i; | |
| for (i = 0; i < counts.length; i++) total += counts[i]; | |
| if (!total) return 0; | |
| var h = 0, p; | |
| for (i = 0; i < counts.length; i++) { | |
| if (!counts[i]) continue; | |
| p = counts[i] / total; | |
| h -= p * Math.log2(p); | |
| } | |
| return h; | |
| } | |
| return { | |
| GRIDS: GRIDS, gridInfo: gridInfo, gridOf: gridOf, | |
| quantise: quantise, quantiseBlock: quantiseBlock, blockStats: blockStats, | |
| allocate: allocate, zeroChannelTest: zeroChannelTest, | |
| measure: measure, identical: identical, entropy: entropy, | |
| indexBitsFor: indexBitsFor, SCALE_BYTES: SCALE_BYTES | |
| }; | |
| }); | |