You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
555 lines
20 KiB
555 lines
20 KiB
5 years ago
|
export class PositionStats {
|
||
|
AB: number = 0;
|
||
|
AD: number = 0;
|
||
|
A2: number = 0;
|
||
|
|
||
|
constructor(public scale: number) {}
|
||
|
|
||
|
addVariable(v: Variable): void {
|
||
|
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;
|
||
|
}
|
||
|
|
||
|
getPosn(): number {
|
||
|
return (this.AD - this.AB) / this.A2;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export class Constraint {
|
||
|
lm: number;
|
||
|
active: boolean = false;
|
||
|
unsatisfiable: boolean = false;
|
||
|
|
||
|
constructor(public left: Variable, public right: Variable, public gap: number, public equality: boolean = false) {
|
||
|
this.left = left;
|
||
|
this.right = right;
|
||
|
this.gap = gap;
|
||
|
this.equality = equality;
|
||
|
}
|
||
|
|
||
|
slack(): number {
|
||
|
return this.unsatisfiable ? Number.MAX_VALUE
|
||
|
: this.right.scale * this.right.position() - this.gap
|
||
|
- this.left.scale * this.left.position();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export class Variable {
|
||
|
offset: number = 0;
|
||
|
block: Block;
|
||
|
cIn: Constraint[];
|
||
|
cOut: Constraint[];
|
||
|
|
||
|
constructor(public desiredPosition: number, public weight: number = 1, public scale: number = 1) {}
|
||
|
|
||
|
dfdv(): number {
|
||
|
return 2.0 * this.weight * (this.position() - this.desiredPosition);
|
||
|
}
|
||
|
|
||
|
position(): number {
|
||
|
return (this.block.ps.scale * this.block.posn + this.offset) / this.scale;
|
||
|
}
|
||
|
|
||
|
// visit neighbours by active constraints within the same block
|
||
|
visitNeighbours(prev: Variable, f: (c: Constraint, next: Variable) => void ): void {
|
||
|
var ff = (c, next) => c.active && prev !== next && f(c, next);
|
||
|
this.cOut.forEach(c=> ff(c, c.right));
|
||
|
this.cIn.forEach(c=> ff(c, c.left));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export class Block {
|
||
|
vars: Variable[] = [];
|
||
|
posn: number;
|
||
|
ps: PositionStats;
|
||
|
blockInd: number;
|
||
|
|
||
|
constructor(v: Variable) {
|
||
|
v.offset = 0;
|
||
|
this.ps = new PositionStats(v.scale);
|
||
|
this.addVariable(v);
|
||
|
}
|
||
|
|
||
|
private addVariable(v: Variable): void {
|
||
|
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
|
||
|
updateWeightedPosition(): void {
|
||
|
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();
|
||
|
}
|
||
|
|
||
|
private compute_lm(v: Variable, u: Variable, postAction: (c: Constraint)=>void): number {
|
||
|
var dfdv = v.dfdv();
|
||
|
v.visitNeighbours(u, (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;
|
||
|
}
|
||
|
|
||
|
private populateSplitBlock(v: Variable, prev: Variable): void {
|
||
|
v.visitNeighbours(prev, (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
|
||
|
traverse(visit: (c: Constraint) => any, acc: any[], v: Variable = this.vars[0], prev: Variable=null) {
|
||
|
v.visitNeighbours(prev, (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.
|
||
|
findMinLM(): Constraint {
|
||
|
var m: Constraint = null;
|
||
|
this.compute_lm(this.vars[0], null, c=> {
|
||
|
if (!c.equality && (m === null || c.lm < m.lm)) m = c;
|
||
|
});
|
||
|
return m;
|
||
|
}
|
||
|
|
||
|
private findMinLMBetween(lv: Variable, rv: Variable): Constraint {
|
||
|
this.compute_lm(lv, null, () => {});
|
||
|
var m = null;
|
||
|
this.findPath(lv, null, rv, (c, next)=> {
|
||
|
if (!c.equality && c.right === next && (m === null || c.lm < m.lm)) m = c;
|
||
|
});
|
||
|
return m;
|
||
|
}
|
||
|
|
||
|
private findPath(v: Variable, prev: Variable, to: Variable, visit: (c: Constraint, next:Variable)=>void): boolean {
|
||
|
var endFound = false;
|
||
|
v.visitNeighbours(prev, (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.
|
||
|
isActiveDirectedPathBetween(u: Variable, v: Variable) : boolean {
|
||
|
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
|
||
|
static split(c: Constraint): Block[]{
|
||
|
/* 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)];
|
||
|
}
|
||
|
|
||
|
private static createSplitBlock(startVar: Variable): Block {
|
||
|
var b = new Block(startVar);
|
||
|
b.populateSplitBlock(startVar, null);
|
||
|
return b;
|
||
|
}
|
||
|
|
||
|
// find a split point somewhere between the specified variables
|
||
|
splitBetween(vl: Variable, vr: Variable): { constraint: Constraint; lb: Block; rb: Block } {
|
||
|
/* 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;
|
||
|
}
|
||
|
|
||
|
mergeAcross(b: Block, c: Constraint, dist: number): void {
|
||
|
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();
|
||
|
}
|
||
|
|
||
|
cost(): number {
|
||
|
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;
|
||
|
}
|
||
|
|
||
|
/* DEBUG
|
||
|
toString(): string {
|
||
|
var cs = [];
|
||
|
this.traverse(c=> c.toString() + "\n", cs)
|
||
|
return "b"+this.blockInd + "@" + this.posn + ": vars=" + this.vars.map(v=> v.toString()+":"+v.offset) + ";\n cons=\n" + cs;
|
||
|
}
|
||
|
DEBUG */
|
||
|
}
|
||
|
|
||
|
export class Blocks {
|
||
|
private list: Block[];
|
||
|
|
||
|
constructor(public vs: Variable[]) {
|
||
|
var n = vs.length;
|
||
|
this.list = new Array(n);
|
||
|
while (n--) {
|
||
|
var b = new Block(vs[n]);
|
||
|
this.list[n] = b;
|
||
|
b.blockInd = n;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
cost(): number {
|
||
|
var sum = 0, i = this.list.length;
|
||
|
while (i--) sum += this.list[i].cost();
|
||
|
return sum;
|
||
|
}
|
||
|
|
||
|
insert(b: Block) {
|
||
|
/* 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 */
|
||
|
}
|
||
|
|
||
|
remove(b: Block) {
|
||
|
/* 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;
|
||
|
/* DEBUG
|
||
|
console.assert(this.contains(swapBlock));
|
||
|
DEBUG */
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// merge the blocks on either side of the specified constraint, by copying the smaller block into the larger
|
||
|
// and deleting the smaller.
|
||
|
merge(c: Constraint): void {
|
||
|
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 */
|
||
|
}
|
||
|
|
||
|
forEach(f: (b: Block, i: number) => void ) {
|
||
|
this.list.forEach(f);
|
||
|
}
|
||
|
|
||
|
// useful, for example, after variable desired positions change.
|
||
|
updateBlockPositions(): void {
|
||
|
this.list.forEach(b=> b.updateWeightedPosition());
|
||
|
}
|
||
|
|
||
|
// split each block across its constraint with the minimum lagrangian
|
||
|
split(inactive: Constraint[]): void {
|
||
|
this.updateBlockPositions();
|
||
|
this.list.forEach(b=> {
|
||
|
var v = b.findMinLM();
|
||
|
if (v !== null && v.lm < Solver.LAGRANGIAN_TOLERANCE) {
|
||
|
b = v.left.block;
|
||
|
Block.split(v).forEach(nb=>this.insert(nb));
|
||
|
this.remove(b);
|
||
|
inactive.push(v);
|
||
|
/* DEBUG
|
||
|
console.assert(this.contains(v.left.block));
|
||
|
console.assert(this.contains(v.right.block));
|
||
|
DEBUG */
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/* DEBUG
|
||
|
// checks b is in the block, and does a sanity check over list index integrity
|
||
|
contains(b: Block): boolean {
|
||
|
var result = false;
|
||
|
this.list.forEach((bb, i) => {
|
||
|
if (bb.blockInd !== i) {
|
||
|
console.error("blocks error, blockInd " + b.blockInd + " found at " + i);
|
||
|
return false;
|
||
|
}
|
||
|
result = result || b === bb;
|
||
|
});
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
toString(): string {
|
||
|
return this.list.toString();
|
||
|
}
|
||
|
DEBUG */
|
||
|
}
|
||
|
|
||
|
export class Solver {
|
||
|
bs: Blocks;
|
||
|
inactive: Constraint[];
|
||
|
|
||
|
static LAGRANGIAN_TOLERANCE = -1e-4;
|
||
|
static ZERO_UPPERBOUND = -1e-10;
|
||
|
|
||
|
constructor(public vs: Variable[], public cs: Constraint[]) {
|
||
|
this.vs = vs;
|
||
|
vs.forEach(v => {
|
||
|
v.cIn = [], v.cOut = [];
|
||
|
/* DEBUG
|
||
|
v.toString = () => "v" + vs.indexOf(v);
|
||
|
DEBUG */
|
||
|
});
|
||
|
this.cs = cs;
|
||
|
cs.forEach(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(c=> { c.active = false; return c; });
|
||
|
this.bs = null;
|
||
|
}
|
||
|
|
||
|
cost(): number {
|
||
|
return this.bs.cost();
|
||
|
}
|
||
|
|
||
|
// set starting positions without changing desired positions.
|
||
|
// Note: it throws away any previous block structure.
|
||
|
setStartingPositions(ps: number[]): void {
|
||
|
this.inactive = this.cs.map(c=> { c.active = false; return c; });
|
||
|
this.bs = new Blocks(this.vs);
|
||
|
this.bs.forEach((b, i) => b.posn = ps[i]);
|
||
|
}
|
||
|
|
||
|
setDesiredPositions(ps: number[]): void {
|
||
|
this.vs.forEach((v, i) => 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 */
|
||
|
|
||
|
private mostViolated(): Constraint {
|
||
|
var minSlack = Number.MAX_VALUE,
|
||
|
v: Constraint = 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
|
||
|
satisfy(): void {
|
||
|
if (this.bs == null) {
|
||
|
this.bs = new Blocks(this.vs);
|
||
|
}
|
||
|
/* DEBUG
|
||
|
console.log("satisfy: " + this.bs);
|
||
|
DEBUG */
|
||
|
this.bs.split(this.inactive);
|
||
|
var v: Constraint = 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.bs.contains(v.left.block);
|
||
|
this.bs.contains(v.right.block);
|
||
|
this.checkInactive();
|
||
|
DEBUG */
|
||
|
}
|
||
|
/* DEBUG
|
||
|
this.checkSatisfied();
|
||
|
DEBUG */
|
||
|
}
|
||
|
|
||
|
// repeatedly build and split block structure until we converge to an optimal solution
|
||
|
solve(): number {
|
||
|
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;
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* Remove overlap between spans while keeping their centers as close as possible to the specified desiredCenters.
|
||
|
* Lower and upper bounds will be respected if the spans physically fit between them
|
||
|
* (otherwise they'll be moved and their new position returned).
|
||
|
* If no upper/lower bound is specified then the bounds of the moved spans will be returned.
|
||
|
* returns a new center for each span.
|
||
|
*/
|
||
|
export function removeOverlapInOneDimension(spans: { size: number, desiredCenter: number }[], lowerBound?: number, upperBound?: number)
|
||
|
: { newCenters: number[], lowerBound: number, upperBound: number }
|
||
|
{
|
||
|
const vs: Variable[] = spans.map(s => new Variable(s.desiredCenter));
|
||
|
const cs: Constraint[] = [];
|
||
|
const n = spans.length;
|
||
|
for (var i = 0; i < n - 1; i++) {
|
||
|
const left = spans[i], right = spans[i + 1];
|
||
|
cs.push(new Constraint(vs[i], vs[i + 1], (left.size + right.size) / 2));
|
||
|
}
|
||
|
const leftMost = vs[0],
|
||
|
rightMost = vs[n - 1],
|
||
|
leftMostSize = spans[0].size / 2,
|
||
|
rightMostSize = spans[n - 1].size / 2;
|
||
|
let vLower: Variable = null, vUpper: Variable = null;
|
||
|
if (lowerBound) {
|
||
|
vLower = new Variable(lowerBound, leftMost.weight * 1000);
|
||
|
vs.push(vLower);
|
||
|
cs.push(new Constraint(vLower, leftMost, leftMostSize));
|
||
|
}
|
||
|
if (upperBound) {
|
||
|
vUpper = new Variable(upperBound, rightMost.weight * 1000);
|
||
|
vs.push(vUpper);
|
||
|
cs.push(new Constraint(rightMost, vUpper, rightMostSize));
|
||
|
}
|
||
|
var solver = new Solver(vs, cs);
|
||
|
solver.solve();
|
||
|
return {
|
||
|
newCenters: vs.slice(0, spans.length).map(v => v.position()),
|
||
|
lowerBound: vLower ? vLower.position() : leftMost.position() - leftMostSize,
|
||
|
upperBound: vUpper ? vUpper.position() : rightMost.position() + rightMostSize
|
||
|
};
|
||
|
}
|