research/erasure_code/share.js

479 lines
16 KiB
JavaScript

(function() {
var me = {};
function ZeroDivisionError() {
if (!this) return new ZeroDivisionError();
this.message = "division by zero";
this.name = "ZeroDivisionError";
}
me.ZeroDivisionError = ZeroDivisionError;
// per-byte 2^8 Galois field
// Note that this imposes a hard limit that the number of extended chunks can
// be at most 256 along each dimension
function galoistpl(a) {
// 2 is not a primitive root, so we have to use 3 as our logarithm base
var r = a ^ (a<<1); // a * (x+1)
if (r > 0xff) { // overflow?
r = r ^ 0x11b;
}
return r;
}
// Precomputing a multiplication and XOR table for increased speed
var glogtable = new Array(256);
var gexptable = [];
(function() {
var v = 1;
for (var i = 0; i < 255; i++) {
glogtable[v] = i;
gexptable.push(v);
v = galoistpl(v);
}
})();
me.glogtable = glogtable;
me.gexptable = gexptable;
function Galois(val) {
if (!(this instanceof Galois)) return new Galois(val);
if (val instanceof Galois) {
this.val = val.val;
} else {
this.val = val;
}
if (typeof Object.freeze == 'function') {
Object.freeze(this);
}
}
me.Galois = Galois;
Galois.prototype.add = Galois.prototype.sub = function(other) {
return new Galois(this.val ^ other.val);
};
Galois.prototype.mul = function(other) {
if (this.val == 0 || other.val == 0) {
return new Galois(0);
}
return new Galois(gexptable[(glogtable[this.val] +
glogtable[other.val]) % 255]);
};
Galois.prototype.div = function(other) {
if (other.val == 0) {
throw new ZeroDivisionError();
}
if (this.val == 0) {
return new Galois(0);
}
return new Galois(gexptable[(glogtable[this.val] + 255 -
glogtable[other.val]) % 255]);
};
Galois.prototype.inspect = function() {
return ""+this.val;
};
function powmod(b, e, m) {
var r = 1;
while (e > 0) {
if (e & 1) r = (r * b) % m;
b = (b * b) % m;
e = e >> 1;
}
return r;
}
// Modular division class
function mkModuloClass(n) {
if (n <= 2) throw new Error("n must be prime!");
for (var divisor = 2; divisor * divisor <= n; divisor++) {
if (n % divisor == 0) {
throw new Error("n must be prime!");
}
}
function Mod(val) {
if (!(this instanceof Mod)) return new Mod(val);
if (val instanceof Mod) {
this.val = val.val;
} else {
this.val = val;
}
if (typeof Object.freeze == 'function') {
Object.freeze(this);
}
}
Mod.modulo = n;
Mod.prototype.add = function(other) {
return new Mod((this.val + other.val) % n);
};
Mod.prototype.sub = function(other) {
return new Mod((this.val + n - other.val) % n);
};
Mod.prototype.mul = function(other) {
return new Mod((this.val * other.val) % n);
};
Mod.prototype.div = function(other) {
return new Mod((this.val * powmod(other.val, n-2, n)) % n);
};
Mod.prototype.inspect = function() {
return ""+this.val;
};
return Mod;
}
me.mkModuloClass = mkModuloClass;
// Evaluates a polynomial in little-endian form, eg. x^2 + 3x + 2 = [2, 3, 1]
// (normally I hate little-endian, but in this case dealing with polynomials
// it's justified, since you get the nice property that p[n] is the nth degree
// term of p) at coordinate x, eg. eval_poly_at([2, 3, 1], 5) = 42 if you are
// using float as your arithmetic
function eval_poly_at(p, x) {
var arithmetic = p[0].constructor;
var y = new arithmetic(0);
var x_to_the_i = new arithmetic(1);
for (var i = 0; i < p.length; i++) {
y = y.add(x_to_the_i.mul(p[i]))
x_to_the_i = x_to_the_i.mul(x);
}
return y;
}
me.eval_poly_at = eval_poly_at;
// Given p+1 y values and x values with no errors, recovers the original
// p+1 degree polynomial. For example,
// lagrange_interp([51.0, 59.0, 66.0], [1, 3, 4]) = [50.0, 0, 1.0]
// if you are using float as your arithmetic
function lagrange_interp(pieces, xs) {
var arithmetic = pieces[0].constructor;
var zero = new arithmetic(0);
var one = new arithmetic(1);
// Generate master numerator polynomial
var root = [one];
var i, j;
for (i = 0; i < xs.length; i++) {
root.unshift(zero);
for (j = 0; j < root.length - 1; j++) {
root[j] = root[j].sub(root[j+1].mul(xs[i]));
}
}
// Generate per-value numerator polynomials by dividing the master
// polynomial back by each x coordinate
var nums = [];
for (i = 0; i < xs.length; i++) {
var output = [];
var last = one;
for (j = 2; j < root.length+1; j++) {
output.unshift(last);
if (j != root.length) {
last = root[root.length-j].add(last.mul(xs[i]));
}
}
nums.push(output);
}
// Generate denominators by evaluating numerator polys at their x
var denoms = [];
for (i = 0; i < xs.length; i++) {
var denom = zero;
var x_to_the_j = one;
for (j = 0; j < nums[i].length; j++) {
denom = denom.add(x_to_the_j.mul(nums[i][j]));
x_to_the_j = x_to_the_j.mul(xs[i]);
}
denoms.push(denom);
}
// Generate output polynomial
var b = [];
for (i = 0; i < pieces.length; i++) {
b[i] = zero;
}
for (i = 0; i < xs.length; i++) {
var yslice = pieces[i].div(denoms[i]);
for (j = 0; j < pieces.length; j++) {
b[j] = b[j].add(nums[i][j].mul(yslice));
}
}
return b;
}
me.lagrange_interp = lagrange_interp;
// Compresses two linear equations of length n into one
// equation of length n-1
// Format:
// 3x + 4y = 80 (ie. 3x + 4y - 80 = 0) -> a = [3,4,-80]
// 5x + 2y = 70 (ie. 5x + 2y - 70 = 0) -> b = [5,2,-70]
function elim(a, b) {
var c = [];
for (var i = 1; i < a.length; i++) {
c[i-1] = a[i].mul(b[0]).sub(b[i].mul(a[0]));
}
return c;
}
// Linear equation solver
// Format:
// 3x + 4y = 80, y = 5 (ie. 3x + 4y - 80z = 0, y = 5, z = 1)
// -> coeffs = [3,4,-80], vals = [5,1]
function evaluate(coeffs, vals) {
var arithmetic = coeffs[0].constructor;
var tot = new arithmetic(0);
for (var i = 0; i < vals.length; i++) {
tot = tot.sub(coeffs[i+1].mul(vals[i]));
}
if (coeffs[0].val == 0) {
throw new ZeroDivisionError();
}
return tot.div(coeffs[0]);
}
// Linear equation system solver
// Format:
// ax + by + c = 0, dx + ey + f = 0
// -> [[a, b, c], [d, e, f]]
// eg.
// [[3.0, 5.0, -13.0], [9.0, 1.0, -11.0]] -> [1.0, 2.0]
function sys_solve(eqs) {
var arithmetic = eqs[0][0].constructor;
var one = new arithmetic(1);
var back_eqs = [eqs[0]];
var i;
while (eqs.length > 1) {
var neweqs = [];
for (i = 0; i < eqs.length - 1; i++) {
neweqs.push(elim(eqs[i], eqs[i+1]));
}
eqs = neweqs;
i = 0;
while (i < eqs.length - 1 && eqs[i][0].val == 0) {
i++;
}
back_eqs.unshift(eqs[i]);
}
var kvals = [one];
for (i = 0; i < back_eqs.length; i++) {
kvals.unshift(evaluate(back_eqs[i], kvals));
}
return kvals.slice(0, -1);
}
me.sys_solve = sys_solve;
function polydiv(Q, E) {
var qpoly = Q.slice();
var epoly = E.slice();
var div = [];
while (qpoly.length >= epoly.length) {
div.unshift(qpoly[qpoly.length-1].div(epoly[epoly.length-1]));
for (var i = 2; i < epoly.length + 1; i++) {
qpoly[qpoly.length-i] =
qpoly[qpoly.length-i].sub(div[0].mul(epoly[epoly.length-i]));
}
qpoly.pop();
}
return div;
}
me.polydiv = polydiv;
// Given a set of y coordinates and x coordinates, and the degree of the
// original polynomial, determines the original polynomial even if some of
// the y coordinates are wrong. If m is the minimal number of pieces (ie.
// degree + 1), t is the total number of pieces provided, then the algo can
// handle up to (t-m)/2 errors. See:
// http://en.wikipedia.org/wiki/Berlekamp%E2%80%93Welch_algorithm#Example
// (just skip to my example, the rest of the article sucks imo)
function berlekamp_welch_attempt(pieces, xs, master_degree) {
var error_locator_degree = Math.floor((pieces.length - master_degree - 1) / 2);
var arithmetic = pieces[0].constructor;
var zero = new arithmetic(0);
var one = new arithmetic(1);
// Set up the equations for y[i]E(x[i]) = Q(x[i])
// degree(E) = error_locator_degree
// degree(Q) = master_degree + error_locator_degree - 1
var eqs = [];
var i,j;
for (i = 0; i < 2 * error_locator_degree + master_degree + 1; i++) {
eqs.push([]);
}
for (i = 0; i < 2 * error_locator_degree + master_degree + 1; i++) {
var neg_x_to_the_j = zero.sub(one);
for (j = 0; j < error_locator_degree + master_degree + 1; j++) {
eqs[i].push(neg_x_to_the_j);
neg_x_to_the_j = neg_x_to_the_j.mul(xs[i]);
}
var x_to_the_j = one;
for (j = 0; j < error_locator_degree + 1; j++) {
eqs[i].push(x_to_the_j.mul(pieces[i]));
x_to_the_j = x_to_the_j.mul(xs[i]);
}
}
// Solve 'em
// Assume the top error polynomial term to be one
var errors = error_locator_degree;
var ones = 1;
var polys;
while (errors >= 0) {
try {
polys = sys_solve(eqs);
for (i = 0; i < ones; i++) polys.push(one);
break;
} catch (e) {
if (e instanceof ZeroDivisionError) {
eqs.pop();
for (i = 0; i < eqs.length; i++) {
var eq = eqs[i];
eq[eq.length-2] = eq[eq.length-2].add(eq[eq.length-1]);
eq.pop();
}
errors--;
ones++;
} else {
throw e;
}
}
}
if (errors < 0) {
throw new Error("Not enough data!");
}
// Divide the polynomials
var qpoly = polys.slice(0, error_locator_degree + master_degree + 1);
var epoly = polys.slice(error_locator_degree + master_degree + 1);
var div = polydiv(qpoly, epoly);
// Check
var corrects = 0;
for (i = 0; i < xs.length; i++) {
if (eval_poly_at(div, xs[i]).val == pieces[i].val) {
corrects++;
}
}
if (corrects < master_degree + errors) {
throw new Error("Answer doesn't match (too many errors)!");
}
return div;
}
me.berlekamp_welch_attempt = berlekamp_welch_attempt;
// Extends a list of integers in [0 ... 255] (if using Galois arithmetic) by
// adding n redundant error-correction values
function extend(data, n, arithmetic) {
arithmetic = arithmetic || Galois;
function mk(x) { return new arithmetic(x); }
var data2 = data.map(mk);
var data3 = data.slice();
var xs = [];
var i;
for (i = 0; i < data.length; i++) {
xs.push(new arithmetic(i));
}
var poly = berlekamp_welch_attempt(data2, xs, data.length - 1);
for (i = 0; i < n; i++) {
data3.push(eval_poly_at(poly, new arithmetic(data.length + i)).val);
}
return data3;
}
me.extend = extend;
// Repairs a list of integers in [0 ... 255]. Some integers can be
// erroneous, and you can put null (or undefined) in place of an integer if
// you know that a certain value is defective or missing. Uses the
// Berlekamp-Welch algorithm to do error-correction
function repair(data, datasize, arithmetic) {
arithmetic = arithmetic || Galois;
var vs = [];
var xs = [];
var i;
for (var i = 0; i < data.length; i++) {
if (data[i] != null) {
vs.push(new arithmetic(data[i]));
xs.push(new arithmetic(i));
}
}
var poly = berlekamp_welch_attempt(vs, xs, datasize - 1);
var result = [];
for (i = 0; i < data.length; i++) {
result.push(eval_poly_at(poly, new arithmetic(i)).val);
}
return result;
}
me.repair = repair;
function transpose(xs) {
var ys = [];
for (var i = 0; i < xs[0].length; i++) {
var y = [];
for (var j = 0; j < xs.length; j++) {
y.push(xs[j][i]);
}
ys.push(y);
}
return ys;
}
// Extends a list of bytearrays
// eg. extend_chunks([map(ord, 'hello'), map(ord, 'world')], 2)
// n is the number of redundant error-correction chunks to add
function extend_chunks(data, n, arithmetic) {
arithmetic = arithmetic || Galois;
var o = [];
for (var i = 0; i < data[0].length; i++) {
o.push(extend(data.map(function(x) { return x[i]; }), n, arithmetic));
}
return transpose(o);
}
me.extend_chunks = extend_chunks;
// Repairs a list of bytearrays. Use null in place of a missing array.
// Individual arrays can contain some missing or erroneous data.
function repair_chunks(data, datasize, arithmetic) {
arithmetic = arithmetic || Galois;
var first_nonzero = 0;
while (data[first_nonzero] == null) {
first_nonzero++;
}
var i;
for (i = 0; i < data.length; i++) {
if (data[i] == null) {
data[i] = new Array(data[first_nonzero].length);
}
}
var o = [];
for (i = 0; i < data[0].length; i++) {
o.push(repair(data.map(function(x) { return x[i]; }), datasize, arithmetic));
}
return transpose(o);
}
me.repair_chunks = repair_chunks;
// Extends either a bytearray or a list of bytearrays or a list of lists...
// Used in the cubify method to expand a cube in all dimensions
function deep_extend_chunks(data, n, arithmetic) {
arithmetic = arithmetic || Galois;
if (!(data[0] instanceof Array)) {
return extend(data, n, arithmetic)
} else {
var o = [];
for (var i = 0; i < data[0].length; i++) {
o.push(deep_extend_chunks(
data.map(function(x) { return x[i]; }), n, arithmetic));
}
return transpose(o);
}
}
me.deep_extend_chunks = deep_extend_chunks;
function isObject(o) {
return typeof o == 'object' || typeof o == 'function';
}
if (typeof define == 'function' && typeof define.amd == 'object' && define.amd) {
define(function() {
return me;
});
} else {
done = 0
try {
if (isObject(module)) { module.exports = me; }
else (isObject(window) ? window : this).Erasure = me;
}
catch(e) {
(isObject(window) ? window : this).Erasure = me;
}
}
}.call(this));