static void TestSolver() { var solver = new Solver(); Variable a = solver.AddVariable("a", 0, 2); Variable b = solver.AddVariable("b", 1, 2); solver.AddConstraint(a, b, 2); solver.Solve(); }
void AdjustFixedBorderPositions(Solver solver, double leftBorderWidth, double rightBorderWidth, bool isHorizontal) { // Note: Open == Left, Close == Right. if (this.OpenBorderInfo.IsFixedPosition && this.CloseBorderInfo.IsFixedPosition) { // Both are fixed, so just move them to their specified positions. For FixedPosition // the API is that it's the outer border edge, so add or subtract half the (left|right)BorderWidth // to set the position to the midpoint. Since both borders are fixed, this provides a // limit to the size of the overall node. this.LeftBorderNode.UpdateDesiredPosition( this.OpenBorderInfo.FixedPosition + (leftBorderWidth / 2)); this.RightBorderNode.UpdateDesiredPosition( this.CloseBorderInfo.FixedPosition - (rightBorderWidth / 2)); this.Size = this.CloseBorderInfo.FixedPosition - this.OpenBorderInfo.FixedPosition; this.Position = this.OpenBorderInfo.FixedPosition + (this.Size / 2); } else if (this.OpenBorderInfo.IsFixedPosition || this.CloseBorderInfo.IsFixedPosition) { // One border is fixed and the other isn't. We'll keep the same cluster size, // move the fixed border to its specified position, adjust our midpoint to reflect that, // and then move the unfixed border to be immediately adjacent to the fixed border; the // solver will cause it to be moved to the minimal position satisfying the constraints. if (this.OpenBorderInfo.IsFixedPosition) { // FixedPosition is the outer border edge so add BorderWidth/2 to set it to the Left midpoint. this.LeftBorderNode.UpdateDesiredPosition( this.OpenBorderInfo.FixedPosition + (leftBorderWidth / 2)); this.Position = this.OpenBorderInfo.FixedPosition + (this.Size / 2); } else /* this.CloseBorderInfo.IsFixedPosition */ { // FixedPosition is the outer border edge so subtract BorderWidth/2 to set it to the Right midpoint. this.RightBorderNode.UpdateDesiredPosition( this.CloseBorderInfo.FixedPosition - (rightBorderWidth / 2)); this.Position = this.CloseBorderInfo.FixedPosition - (this.Size / 2); } } // If we have a minimum size, generate constraints for it. Although this may change the size // considerably, so may the movement of variables in the cluster, so we need no precalculation // of sizes or positions; but after the Horizontal pass, the caller must pass in the resultant // positions in the Horizontal (perpendicular) BorderInfos parameter to Vertical generation; // otherwise, because the Horizontal cluster span may be larger than is calculated simply from // variable positions, some variables may not have appropriate constraints generated. if (this.MinimumSize > 0.0) { Constraint cst = solver.AddConstraint( this.LeftBorderNode.Variable, this.RightBorderNode.Variable, this.MinimumSize - leftBorderWidth/2 - rightBorderWidth/2); Debug.Assert(null != cst, "Minimum Cluster size: unexpected null cst"); #if VERBOSE Console.WriteLine(" {0} MinClusterSizeCst {1} -> {2} g {3:F5}", isHorizontal ? "H" : "V" , cst.Left.Name, cst.Right.Name, cst.Gap); #endif // VERBOSE } // Now recalculate our perpendicular PositionP/SizeP if either perpendicular border is fixed, // since we know we're going to move things there. We don't actually create variables for the // perpendicular axis on this pass, but we set the primary axis border nodes' perpendicular size // and position, thus creating "virtual" perpendicular borders used by the parent cluster's // Generate() and for its events in its GenerateFromEvents(). This must be done on both H and V // passes, because multiple heavyweight Fixed borders can push each other around on the horizontal // pass and leave excessive space between the fixed border and the outer nodes. In that case the // Vertical pass can't get the true X border positions by evaluating our nodes' X positions; the // caller must pass this updated position in (the same thing it must do for nodes' X coordinates). if (this.OpenBorderInfoP.IsFixedPosition || this.CloseBorderInfoP.IsFixedPosition) { // If both are fixed, we'll set to those positions and recalculate size. // Remember that FixedPosition API is the outer border edge so we don't need to adjust for border width. if (this.OpenBorderInfoP.IsFixedPosition && this.CloseBorderInfoP.IsFixedPosition) { this.SizeP = this.CloseBorderInfoP.FixedPosition - this.OpenBorderInfoP.FixedPosition; this.PositionP = this.OpenBorderInfoP.FixedPosition + (this.SizeP / 2); if (this.SizeP < 0) { // Open border is to the right of close border; they'll move later, but we have to // make the size non-negative. TODOunit: create a specific test for this (fixed LRTB) this.SizeP = -this.SizeP; } } else { // Only one is fixed, so we'll adjust in the appropriate direction as needed. // - If we're on the horizontal pass we'll preserve the above calculation of this.SizeP // and only shift things around to preserve the relative vertical starting positions; // running the Solver will change these positions. // - If we're on the vertical pass, we know the horizontal nodes are in their final positions, // so we need to accommodate the case described above, where the Solver opened up space // between the fixed border and the outermost nodes (it will never *reduce* this distance // of course). This means we adjust both border position and our overall node size. double curTopOuterBorder = this.PositionP - (this.SizeP / 2); double curBottomOuterBorder = this.PositionP + (this.SizeP / 2); if (this.OpenBorderInfoP.IsFixedPosition) { if (isHorizontal) { // Don't change SizeP. this.PositionP += this.OpenBorderInfoP.FixedPosition - curTopOuterBorder; } else { this.SizeP = curBottomOuterBorder - this.OpenBorderInfoP.FixedPosition; this.PositionP = this.OpenBorderInfoP.FixedPosition + (this.SizeP / 2); } } else { if (isHorizontal) { // Don't change SizeP. this.PositionP += this.CloseBorderInfoP.FixedPosition - curBottomOuterBorder; } else { this.SizeP = this.CloseBorderInfoP.FixedPosition - curTopOuterBorder; this.PositionP = curTopOuterBorder + (this.SizeP / 2); } } } // endifelse both borders fixed or only one border is // Now update our fake border nodes' PositionP/SizeP to be consistent. this.LeftBorderNode.PositionP = this.PositionP; this.LeftBorderNode.SizeP = this.SizeP; this.RightBorderNode.PositionP = this.PositionP; this.RightBorderNode.SizeP = this.SizeP; } }
void GenerateFromEvents(Solver solver, OverlapRemovalParameters parameters, List<Event> events, bool isHorizontal) { // First, sort the events on the perpendicular coordinate of the event // (e.g. for horizontal constraint generation, order on vertical position). events.Sort(); #if VERBOSE Console.WriteLine("Events:"); foreach (Event evt in events) { Console.WriteLine(" {0}", evt); } #endif // VERBOSE var scanLine = new ScanLine(); foreach (Event evt in events) { OverlapRemovalNode currentNode = evt.Node; if (evt.IsForOpen) { // Insert the current node into the scan line. scanLine.Insert(currentNode); #if VERBOSE Console.WriteLine("ScanAdd: {0}", currentNode); #endif // VERBOSE // Find the nodes that are currently open to either side of it and are either overlapping // nodes or the first non-overlapping node in that direction. currentNode.LeftNeighbors = GetLeftNeighbours(parameters, scanLine, currentNode, isHorizontal); currentNode.RightNeighbors = GetRightNeighbours(parameters, scanLine, currentNode, isHorizontal); // Use counts for indexing for performance (rather than foreach, and hoist the count-control // variable out of the loop so .Count isn't checked on each iteration, since we know it's // not going to be changed). int numLeftNeighbors = currentNode.LeftNeighbors.Count; int numRightNeighbors = currentNode.RightNeighbors.Count; // If there is currently a non-overlap constraint between any two nodes across the // two neighbour lists we've just created, we can remove them because they will be // transitively enforced by the constraints we'll create for the current node. // I.e., we can remove the specification for the constraint // leftNeighborNode + gap + padding <= rightNeighborNode // because it is implied by the constraints we'll create for // leftNeighborNode + gap + padding <= node // node + gap + padding <= rightNeighborNode // We must also add the current node as a neighbour in the appropriate direction. // @@PERF: List<T>.Remove is a sequential search so averages 1/2 the size of the // lists. We currently don't expect the neighbour lists to be very large or Remove // to be a frequent operation, and using HashSets would incur the GetEnumerator overhead // on the outer and inner loops; but .Remove creates an inner-inner loop so do some // timing runs to compare performance. // @@PERF: handles the case where we are node c and have added node b as a lnbour // and node d as rnbour, where those nodes are already nbours. But it does not handle // the case where we add node b and node a as lnbours, and node b already has node a // as an lnbour. To do this I think we'd just want to skip adding the node-a lnbour, // but that forms a new inner loop (iterating all lnbours before adding a new one) // unless we develop different storage for nbours. for (int ii = 0; ii < numLeftNeighbors; ++ii) { OverlapRemovalNode leftNeighborNode = currentNode.LeftNeighbors[ii]; for (int jj = 0; jj < numRightNeighbors; ++jj) { // TODOunit: test this OverlapRemovalNode nodeToRemove = currentNode.RightNeighbors[jj]; if (leftNeighborNode.RightNeighbors.Remove(nodeToRemove)) { #if VERBOSE Console.WriteLine(" {0} RnbourRem {1} --> {2}", isHorizontal ? "H" : "V", leftNeighborNode, nodeToRemove); #endif // VERBOSE } } leftNeighborNode.RightNeighbors.Add(currentNode); } for (int ii = 0; ii < numRightNeighbors; ++ii) { // TODOunit: test this OverlapRemovalNode rightNeighborNode = currentNode.RightNeighbors[ii]; for (int jj = 0; jj < numLeftNeighbors; ++jj) { OverlapRemovalNode nodeToRemove = currentNode.LeftNeighbors[jj]; if (rightNeighborNode.LeftNeighbors.Remove(nodeToRemove)) { #if VERBOSE Console.WriteLine(" {0} LnbourRem {1} --> {2}", isHorizontal ? "H" : "V", nodeToRemove, rightNeighborNode); #endif // VERBOSE } } rightNeighborNode.LeftNeighbors.Add(currentNode); } } // endif evt.IsForOpen else { // This is a close event, so generate the constraints and remove the closing node // from its neighbours lists. If we're closing we should have left neighbours so // this is null then we've likely got some sort of internal calculation error. if (null == currentNode.LeftNeighbors) { Debug.Assert(null != currentNode.LeftNeighbors, "LeftNeighbors should not be null for a Close event"); continue; } // currentNode is the current node; if it's a cluster, translate it to the node that // should be involved in the constraint (if it's the left neighbour then use its // right border as the constraint variable, and vice-versa). OverlapRemovalNode currentLeftNode = GetLeftConstraintNode(currentNode); OverlapRemovalNode currentRightNode = GetRightConstraintNode(currentNode); // LeftNeighbors must end before the current node... int cLeftNeighbours = currentNode.LeftNeighbors.Count; for (int ii = 0; ii < cLeftNeighbours; ++ii) { // Keep track of the original Node; it may be the base of a Cluster, in which // case it will have the active neighbours list, not leftNeighborNode (which will // be the left border "fake Node"). OverlapRemovalNode origLeftNeighborNode = currentNode.LeftNeighbors[ii]; origLeftNeighborNode.RightNeighbors.Remove(currentNode); OverlapRemovalNode leftNeighborNode = GetLeftConstraintNode(origLeftNeighborNode); Debug.Assert(leftNeighborNode.OpenP == origLeftNeighborNode.OpenP, "leftNeighborNode.OpenP must == origLeftNeighborNode.OpenP"); // This assert verifies we match the Solver.ViolationTolerance check in AddNeighbor. // We are closing the node here so use an alternative to OverlapP for additional // consistency verification. Allow a little rounding error. Debug.Assert(isHorizontal || ((currentNode.CloseP + NodePaddingP - leftNeighborNode.OpenP) > (parameters.SolverParameters.GapTolerance - 1e-6)), "LeftNeighbors: unexpected close/open overlap"); double p = leftNeighborNode == LeftBorderNode || currentRightNode == RightBorderNode ? ClusterPadding : NodePadding; double separation = ((leftNeighborNode.Size + currentRightNode.Size) / 2) + p; if (TranslateChildren) { separation = Math.Max(separation, currentRightNode.Position - leftNeighborNode.Position); } Constraint cst = solver.AddConstraint(leftNeighborNode.Variable, currentRightNode.Variable, separation); Debug.Assert(null != cst, "LeftNeighbors: unexpected null cst"); #if VERBOSE Console.WriteLine(" {0} LnbourCst {1} -> {2} g {3:F5}", isHorizontal ? "H" : "V" , cst.Left.Name, cst.Right.Name, cst.Gap); #endif // VERBOSE } // ... and RightNeighbors must start after the current node. int cRightNeighbours = currentNode.RightNeighbors.Count; for (int ii = 0; ii < cRightNeighbours; ++ii) { // Keep original node, which may be a cluster; see comments in LeftNeighbors above. OverlapRemovalNode origRightNeighborNode = currentNode.RightNeighbors[ii]; origRightNeighborNode.LeftNeighbors.Remove(currentNode); OverlapRemovalNode rightNeighborNode = GetRightConstraintNode(origRightNeighborNode); // This assert verifies we match the Solver.ViolationTolerance check in AddNeighbor. // Allow a little rounding error. Debug.Assert(isHorizontal || ((currentNode.CloseP + NodePaddingP - rightNeighborNode.OpenP) > (parameters.SolverParameters.GapTolerance - 1e-6)), "RightNeighbors: unexpected close/open overlap"); double p = currentLeftNode == LeftBorderNode || rightNeighborNode == RightBorderNode ? ClusterPadding : NodePadding; double separation = ((currentLeftNode.Size + rightNeighborNode.Size) / 2) + p; if (TranslateChildren) { separation = Math.Max(separation, rightNeighborNode.Position - currentLeftNode.Position); } Constraint cst = solver.AddConstraint(currentLeftNode.Variable, rightNeighborNode.Variable, separation); Debug.Assert(null != cst, "RightNeighbors: unexpected null cst"); #if VERBOSE Console.WriteLine(" {0} RnbourCst {1} -> {2} g {3:F5}", isHorizontal ? "H" : "V" , cst.Left.Name, cst.Right.Name, cst.Gap); #endif // VERBOSE } // Note: although currentNode is closed, there may still be open nodes in its // Neighbour lists; these will subsequently be processed (and removed from // currentNode.*Neighbour) when those Neighbors are closed. scanLine.Remove(currentNode); #if VERBOSE Console.WriteLine("ScanRem: {0}", currentNode); #endif // VERBOSE } // endelse !evt.IsForOpen // @@PERF: Set Node.Left/RightNeighbors null to let the GC know we're not using them // anymore, unless we can reasonably assume a short lifetime for the ConstraintGenerator. } // endforeach Event }
public void Test_MoveMidVarsToSegmentEnds_PushIn() { var solver = new Solver(); const double FixedVarWeight = 1e8; var heavy0 = solver.AddVariable(null, 0, FixedVarWeight); var heavy1 = solver.AddVariable(null, 2, FixedVarWeight); var light0 = solver.AddVariable(null, 1); var light1 = solver.AddVariable(null, 1); solver.AddConstraint(light0, heavy0, 0); solver.AddConstraint(heavy1, light1, 0); solver.AddNeighborPair(light0, light1, 1 / FixedVarWeight); solver.Solve(); // expected values const double Heavy0Expected = 1.00000001E-08; const double Light0Expected = 1.00000001E-08; const double Light1Expected = 1.99999999; const double Heavy1Expected = 1.99999999; if (!ApproxEquals(heavy0.ActualPos, Heavy0Expected) || !ApproxEquals(light0.ActualPos, Light0Expected) || !ApproxEquals(light1.ActualPos, Light1Expected) || !ApproxEquals(heavy1.ActualPos, Heavy1Expected)) { if (TestGlobals.VerboseLevel > 0) { WriteLine("Failed - actual/expected: h0={0}/{1} l0={2}/{3} l1={4}/{5} h1={6}/{7}", heavy0.ActualPos, Heavy0Expected, light0.ActualPos, Light0Expected, light1.ActualPos, Light1Expected, heavy1.ActualPos, Heavy1Expected); } Validate.Fail("Results were not as expected"); } }