src/lib/vpsc.js (439 lines of code) (raw):
/*
This file is compiled from https://github.com/tgdwyer/WebCola/blob/master/WebCola/src/vpsc.ts
and modified slightly.
The MIT License (MIT)
Copyright (c) 2013 Tim Dwyer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
var vpsc = {};
var PositionStats = (function () {
function PositionStats(scale) {
this.scale = scale;
this.AB = 0;
this.AD = 0;
this.A2 = 0;
}
PositionStats.prototype.addVariable = function (v) {
var ai = this.scale / v.scale;
var bi = v.offset / v.scale;
var wi = v.weight;
this.AB += wi * ai * bi;
this.AD += wi * ai * v.desiredPosition;
this.A2 += wi * ai * ai;
};
PositionStats.prototype.getPosn = function () {
return (this.AD - this.AB) / this.A2;
};
return PositionStats;
})();
vpsc.PositionStats = PositionStats;
var Constraint = (function () {
function Constraint(left, right, gap, equality) {
if (equality === void 0) { equality = false; }
this.left = left;
this.right = right;
this.gap = gap;
this.equality = equality;
this.active = false;
this.unsatisfiable = false;
this.left = left;
this.right = right;
this.gap = gap;
this.equality = equality;
}
Constraint.prototype.slack = function () {
return this.unsatisfiable ? Number.MAX_VALUE
: this.right.scale * this.right.position() - this.gap
- this.left.scale * this.left.position();
};
return Constraint;
})();
vpsc.Constraint = Constraint;
var Variable = (function () {
function Variable(desiredPosition, weight, scale) {
if (weight === void 0) { weight = 1; }
if (scale === void 0) { scale = 1; }
this.desiredPosition = desiredPosition;
this.weight = weight;
this.scale = scale;
this.offset = 0;
}
Variable.prototype.dfdv = function () {
return 2.0 * this.weight * (this.position() - this.desiredPosition);
};
Variable.prototype.position = function () {
return (this.block.ps.scale * this.block.posn + this.offset) / this.scale;
};
// visit neighbours by active constraints within the same block
Variable.prototype.visitNeighbours = function (prev, f) {
var ff = function (c, next) { return c.active && prev !== next && f(c, next); };
this.cOut.forEach(function (c) { return ff(c, c.right); });
this.cIn.forEach(function (c) { return ff(c, c.left); });
};
return Variable;
})();
vpsc.Variable = Variable;
var Block = (function () {
function Block(v) {
this.vars = [];
v.offset = 0;
this.ps = new PositionStats(v.scale);
this.addVariable(v);
}
Block.prototype.addVariable = function (v) {
v.block = this;
this.vars.push(v);
this.ps.addVariable(v);
this.posn = this.ps.getPosn();
};
// move the block where it needs to be to minimize cost
Block.prototype.updateWeightedPosition = function () {
this.ps.AB = this.ps.AD = this.ps.A2 = 0;
for (var i = 0, n = this.vars.length; i < n; ++i)
this.ps.addVariable(this.vars[i]);
this.posn = this.ps.getPosn();
};
Block.prototype.compute_lm = function (v, u, postAction) {
var _this = this;
var dfdv = v.dfdv();
v.visitNeighbours(u, function (c, next) {
var _dfdv = _this.compute_lm(next, v, postAction);
if (next === c.right) {
dfdv += _dfdv * c.left.scale;
c.lm = _dfdv;
}
else {
dfdv += _dfdv * c.right.scale;
c.lm = -_dfdv;
}
postAction(c);
});
return dfdv / v.scale;
};
Block.prototype.populateSplitBlock = function (v, prev) {
var _this = this;
v.visitNeighbours(prev, function (c, next) {
next.offset = v.offset + (next === c.right ? c.gap : -c.gap);
_this.addVariable(next);
_this.populateSplitBlock(next, v);
});
};
// traverse the active constraint tree applying visit to each active constraint
Block.prototype.traverse = function (visit, acc, v, prev) {
var _this = this;
if (v === void 0) { v = this.vars[0]; }
if (prev === void 0) { prev = null; }
v.visitNeighbours(prev, function (c, next) {
acc.push(visit(c));
_this.traverse(visit, acc, next, v);
});
};
// calculate lagrangian multipliers on constraints and
// find the active constraint in this block with the smallest lagrangian.
// if the lagrangian is negative, then the constraint is a split candidate.
Block.prototype.findMinLM = function () {
var m = null;
this.compute_lm(this.vars[0], null, function (c) {
if (!c.equality && (m === null || c.lm < m.lm))
m = c;
});
return m;
};
Block.prototype.findMinLMBetween = function (lv, rv) {
this.compute_lm(lv, null, function () { });
var m = null;
this.findPath(lv, null, rv, function (c, next) {
if (!c.equality && c.right === next && (m === null || c.lm < m.lm))
m = c;
});
return m;
};
Block.prototype.findPath = function (v, prev, to, visit) {
var _this = this;
var endFound = false;
v.visitNeighbours(prev, function (c, next) {
if (!endFound && (next === to || _this.findPath(next, v, to, visit))) {
endFound = true;
visit(c, next);
}
});
return endFound;
};
// Search active constraint tree from u to see if there is a directed path to v.
// Returns true if path is found.
Block.prototype.isActiveDirectedPathBetween = function (u, v) {
if (u === v)
return true;
var i = u.cOut.length;
while (i--) {
var c = u.cOut[i];
if (c.active && this.isActiveDirectedPathBetween(c.right, v))
return true;
}
return false;
};
// split the block into two by deactivating the specified constraint
Block.split = function (c) {
/* DEBUG
console.log("split on " + c);
console.assert(c.active, "attempt to split on inactive constraint");
DEBUG */
c.active = false;
return [Block.createSplitBlock(c.left), Block.createSplitBlock(c.right)];
};
Block.createSplitBlock = function (startVar) {
var b = new Block(startVar);
b.populateSplitBlock(startVar, null);
return b;
};
// find a split point somewhere between the specified variables
Block.prototype.splitBetween = function (vl, vr) {
/* DEBUG
console.assert(vl.block === this);
console.assert(vr.block === this);
DEBUG */
var c = this.findMinLMBetween(vl, vr);
if (c !== null) {
var bs = Block.split(c);
return { constraint: c, lb: bs[0], rb: bs[1] };
}
// couldn't find a split point - for example the active path is all equality constraints
return null;
};
Block.prototype.mergeAcross = function (b, c, dist) {
c.active = true;
for (var i = 0, n = b.vars.length; i < n; ++i) {
var v = b.vars[i];
v.offset += dist;
this.addVariable(v);
}
this.posn = this.ps.getPosn();
};
Block.prototype.cost = function () {
var sum = 0, i = this.vars.length;
while (i--) {
var v = this.vars[i], d = v.position() - v.desiredPosition;
sum += d * d * v.weight;
}
return sum;
};
return Block;
})();
vpsc.Block = Block;
var Blocks = (function () {
function Blocks(vs) {
this.vs = vs;
var n = vs.length;
this.list = new Array(n);
while (n--) {
var b = new Block(vs[n]);
this.list[n] = b;
b.blockInd = n;
}
}
Blocks.prototype.cost = function () {
var sum = 0, i = this.list.length;
while (i--)
sum += this.list[i].cost();
return sum;
};
Blocks.prototype.insert = function (b) {
/* DEBUG
console.assert(!this.contains(b), "blocks error: tried to reinsert block " + b.blockInd)
DEBUG */
b.blockInd = this.list.length;
this.list.push(b);
/* DEBUG
console.log("insert block: " + b.blockInd);
this.contains(b);
DEBUG */
};
Blocks.prototype.remove = function (b) {
/* DEBUG
console.log("remove block: " + b.blockInd);
console.assert(this.contains(b));
DEBUG */
var last = this.list.length - 1;
var swapBlock = this.list[last];
this.list.length = last;
if (b !== swapBlock) {
this.list[b.blockInd] = swapBlock;
swapBlock.blockInd = b.blockInd;
}
};
// merge the blocks on either side of the specified constraint, by copying the smaller block into the larger
// and deleting the smaller.
Blocks.prototype.merge = function (c) {
var l = c.left.block, r = c.right.block;
/* DEBUG
console.assert(l!==r, "attempt to merge within the same block");
DEBUG */
var dist = c.right.offset - c.left.offset - c.gap;
if (l.vars.length < r.vars.length) {
r.mergeAcross(l, c, dist);
this.remove(l);
}
else {
l.mergeAcross(r, c, -dist);
this.remove(r);
}
/* DEBUG
console.assert(Math.abs(c.slack()) < 1e-6, "Error: Constraint should be at equality after merge!");
console.log("merged on " + c);
DEBUG */
};
Blocks.prototype.forEach = function (f) {
this.list.forEach(f);
};
// useful, for example, after variable desired positions change.
Blocks.prototype.updateBlockPositions = function () {
this.list.forEach(function (b) { return b.updateWeightedPosition(); });
};
// split each block across its constraint with the minimum lagrangian
Blocks.prototype.split = function (inactive) {
var _this = this;
this.updateBlockPositions();
this.list.forEach(function (b) {
var v = b.findMinLM();
if (v !== null && v.lm < Solver.LAGRANGIAN_TOLERANCE) {
b = v.left.block;
Block.split(v).forEach(function (nb) { return _this.insert(nb); });
_this.remove(b);
inactive.push(v);
}
});
};
return Blocks;
})();
vpsc.Blocks = Blocks;
var Solver = (function () {
function Solver(vs, cs) {
this.vs = vs;
this.cs = cs;
this.vs = vs;
vs.forEach(function (v) {
v.cIn = [], v.cOut = [];
/* DEBUG
v.toString = () => "v" + vs.indexOf(v);
DEBUG */
});
this.cs = cs;
cs.forEach(function (c) {
c.left.cOut.push(c);
c.right.cIn.push(c);
/* DEBUG
c.toString = () => c.left + "+" + c.gap + "<=" + c.right + " slack=" + c.slack() + " active=" + c.active;
DEBUG */
});
this.inactive = cs.map(function (c) { c.active = false; return c; });
this.bs = null;
}
Solver.prototype.cost = function () {
return this.bs.cost();
};
// set starting positions without changing desired positions.
// Note: it throws away any previous block structure.
Solver.prototype.setStartingPositions = function (ps) {
this.inactive = this.cs.map(function (c) { c.active = false; return c; });
this.bs = new Blocks(this.vs);
this.bs.forEach(function (b, i) { return b.posn = ps[i]; });
};
Solver.prototype.setDesiredPositions = function (ps) {
this.vs.forEach(function (v, i) { return v.desiredPosition = ps[i]; });
};
/* DEBUG
private getId(v: Variable): number {
return this.vs.indexOf(v);
}
// sanity check of the index integrity of the inactive list
checkInactive(): void {
var inactiveCount = 0;
this.cs.forEach(c=> {
var i = this.inactive.indexOf(c);
console.assert(!c.active && i >= 0 || c.active && i < 0, "constraint should be in the inactive list if it is not active: " + c);
if (i >= 0) {
inactiveCount++;
} else {
console.assert(c.active, "inactive constraint not found in inactive list: " + c);
}
});
console.assert(inactiveCount === this.inactive.length, inactiveCount + " inactive constraints found, " + this.inactive.length + "in inactive list");
}
// after every call to satisfy the following should check should pass
checkSatisfied(): void {
this.cs.forEach(c=>console.assert(c.slack() >= vpsc.Solver.ZERO_UPPERBOUND, "Error: Unsatisfied constraint! "+c));
}
DEBUG */
Solver.prototype.mostViolated = function () {
var minSlack = Number.MAX_VALUE, v = null, l = this.inactive, n = l.length, deletePoint = n;
for (var i = 0; i < n; ++i) {
var c = l[i];
if (c.unsatisfiable)
continue;
var slack = c.slack();
if (c.equality || slack < minSlack) {
minSlack = slack;
v = c;
deletePoint = i;
if (c.equality)
break;
}
}
if (deletePoint !== n &&
(minSlack < Solver.ZERO_UPPERBOUND && !v.active || v.equality)) {
l[deletePoint] = l[n - 1];
l.length = n - 1;
}
return v;
};
// satisfy constraints by building block structure over violated constraints
// and moving the blocks to their desired positions
Solver.prototype.satisfy = function () {
if (this.bs == null) {
this.bs = new Blocks(this.vs);
}
/* DEBUG
console.log("satisfy: " + this.bs);
DEBUG */
this.bs.split(this.inactive);
var v = null;
while ((v = this.mostViolated()) && (v.equality || v.slack() < Solver.ZERO_UPPERBOUND && !v.active)) {
var lb = v.left.block, rb = v.right.block;
/* DEBUG
console.log("most violated is: " + v);
this.bs.contains(lb);
this.bs.contains(rb);
DEBUG */
if (lb !== rb) {
this.bs.merge(v);
}
else {
if (lb.isActiveDirectedPathBetween(v.right, v.left)) {
// cycle found!
v.unsatisfiable = true;
continue;
}
// constraint is within block, need to split first
var split = lb.splitBetween(v.left, v.right);
if (split !== null) {
this.bs.insert(split.lb);
this.bs.insert(split.rb);
this.bs.remove(lb);
this.inactive.push(split.constraint);
}
else {
/* DEBUG
console.log("unsatisfiable constraint found");
DEBUG */
v.unsatisfiable = true;
continue;
}
if (v.slack() >= 0) {
/* DEBUG
console.log("violated constraint indirectly satisfied: " + v);
DEBUG */
// v was satisfied by the above split!
this.inactive.push(v);
}
else {
/* DEBUG
console.log("merge after split:");
DEBUG */
this.bs.merge(v);
}
}
}
/* DEBUG
this.checkSatisfied();
DEBUG */
};
// repeatedly build and split block structure until we converge to an optimal solution
Solver.prototype.solve = function () {
this.satisfy();
var lastcost = Number.MAX_VALUE, cost = this.bs.cost();
while (Math.abs(lastcost - cost) > 0.0001) {
this.satisfy();
lastcost = cost;
cost = this.bs.cost();
}
return cost;
};
Solver.LAGRANGIAN_TOLERANCE = -1e-4;
Solver.ZERO_UPPERBOUND = -1e-10;
return Solver;
})();
vpsc.Solver = Solver;
module.exports = vpsc;