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 }; }