Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

libavoid: false assertion in nudgeOrthogonalRoutes() when nudgeSharedPathsWithCommonEndPoint is false #67

Open
skieffer opened this issue Sep 2, 2023 · 1 comment · May be fixed by #68

Comments

@skieffer
Copy link
Collaborator

skieffer commented Sep 2, 2023

Problem

Sometimes, when the routing option nudgeSharedPathsWithCommonEndPoint is set to false, the nudgeOrthogonalRoutes() method of the ImproveOrthogonalRoutes class in libavoid makes the assertion

COLA_ASSERT(vs[i - 1]->id == channelLeftID);

which turns out to be false, and the process crashes.

Review

As a non-author of libavoid, I want to start with a review of how orthogonal connector
nudging works, for the sake of my own understanding as much as anyone else's.

The nudgeOrthogonalRoutes() function works one dimension at a time, and I will
assume throughout that we are working in the y-dimension. This means we are looking at
all of the horizontal connector segments, and we are interested in adjusting their y-coords,
in order to nudge them apart.

Before nudging can begin, each segment is assigned a channel.
The channel for a given horizontal segment is a rectangle whose x-interval equals that of the segment,
and whose y-interval is defined by the min and max allowed y-coords for this segment. These
bounds are defined by the set of all obstacles. The channel thus represents the space
in which the segment is allowed to move up or down in the nudging process. Importantly, either of the
upper or lower bounds may be infinite, in cases where there are no obstacles in the way.

The basic process of nudgeOrthogonalRoutes() is to partition the set of all horizontal segments into
groups whose channels overlap, and then to work one group at a time.
For each group, nudgeOrthogonalRoutes() sets up a vpsc problem, to try to nudge
the segments apart by the desired nudging distance. If the vpsc problem is not satisifed,
it decreases the nudging distance and tries again. It tries at most 10 times, then gives up on that group,
leaving all segments in their original position.

justUnifying

The nudgeOrthogonalRoutes() is actually called twice in the overall routing process, the first time with
a flag, justUnifying = true, which makes it conduct a very different process, explained by the comment here:

// Do Unifying first, by itself. This greedily tries to position free
// segments in overlapping channels at the same position. This way they
// have correct nudging orders determined for them since they will form
// shared paths, rather than segments just positioned as an results of
// the routing process. Of course, don't do this when rerouting with

The vpsc problem

A vpsc problem is defined by a set of variables and a set of constraints. For each group of segments,
we set up the following problem.

Variables

What are the variables in a nudging problem?
They are of four kinds:

  • Free: represents a connector segment that is allowed to wind up anywhere within its channel
  • Fixed: represents a connector segment that is required to stay in its starting position
  • Left Channel: represents the "left" side of a channel (the upper side, when working in the y-dimension)
  • Right Channel: represents the "right" side of a channel (the lower side, when working in the y-dimension)

Constraints

What are the constraints?

  • Free vars must stay between their corrsp. left and right channel vars
  • Separation constraints to achieve the desired nudging
  • Certain equality constraints for special cases defined here

The special case equality constraints -- the third item above -- play a critical role in the issue discussed here.

Weights

The vpsc problem defined in nudgeOrthogonalRoutes() takes a "soft" approach to the variables that are not supposed
to move, which includes all three of the fixed, left, and right variables. Instead of strictly forbidding movement,
with the possibility that vpsc could actually fail, it uses weights. It puts a weight of 100000 on the variables that
should not move, and a weight of 0.001 on the free variables. This means the vpsc problem is always solvable. After it
is solved, nudgeOrthogonalRoutes() assesses whether it was satisfied or not, by checking whether any of the heavy-weight
variables was moved from its desired position by more than a small threshold value (0.0001).

Responding to an unsatisfied vpsc problem

The issue arises in the way nudgeOrthogonalRoutes() responds to an unsatisfied vpsc problem, i.e. one in which one or more
fixed vars was forced to move.

The goal is to determine the set of separation constraints whose gaps should be diminished before trying again.
For this, nudgeOrthogonalRoutes() uses a notion of an "unsat range," a closed interval of indices into the vector of vpsc variables, being those fixed variables that were forced to move.

It is in the process of determining these ranges that the false assertion is made. The expectation expressed here,

// There are no existing unsatisfied ranges,
// so start a new unsatisfied range.
// We are looking at a unsatisfied right side
// where the left side was satisfied, so the
// range begins at the previous variable
// which should be a left channel side.
COLA_ASSERT(i > 0);
COLA_ASSERT(vs[i - 1]->id == channelLeftID);

implies that, if a right channel side had to move, this channel must also have a left side. But this is not always true, as the following example will show. When the corresponding left channel var is absent, the second assertion above fails.

Example

The example routing problem we'll look at succeeds if the routing option nudgeSharedPathsWithCommonEndPoint is true;
it fails with the false assertion when this option is false. In order to be able to visualize and understand the routing problem,
we begin by looking at the successful routing we get when nudgeSharedPathsWithCommonEndPoint is true:

yes_NSPWCE_labeled

By commenting out the problematic assertions, and setting nudgeSharedPathsWithCommonEndPoint back to false, we can see how the routing looks before the nudging attempt:

no_NSPWCE_labeled

In the "no-nudging" image, I have labeled the four different desired y-coords that come up in the vpsc problem (see below), which concerns the segments in the group at the top of the diagram.

In the "with-nudging" image, it is easy to see all the different connectors that were bundled together at these y-coords, before nudging.

Problematic vars and constraints

The vpsc problem constructed for the segments participating in this group has 13 variables, and 23 constraints.

Variables

index, id, desiredPosition, finalPosition, weight,   notes

0,      0,  -516.954,       -503.733,       0.001    ConnRef 49
1,      3,  -516.954,       -503.733,       100000     UNSAT

2,      0,  -516.954,       -507.733,       0.001    ConnRef 27
3,      3,  -516.954,       -507.733,       100000     UNSAT


4,      1,  -485.291,       -507.733,       100000     UNSAT   ConnRef 35


5,      0,  -458.302,       -503.733,       0.001
6,      3,  -458.302,       -458.302,       100000

7,      0,  -458.302,       -507.733,       0.001    ConnRef 48
8,      3,  -458.302,       -458.302,       100000

9,      0,  -458.302,       -507.733,       0.001    ConnRef 41
10,     3,  -458.302,       -458.302,       100000


11,     0,  -431.639,       -507.733,       0.001
12,     3,  -431.639,       -431.639,       100000

Note: the id column indicates which of the four types each variable is:

free=0, fixed=1, left=2, right=3

Notice that there are no left variables in this problem. This simply reflects the fact that, for all of these segments, there is no lower channel boundary, because we are at the top of the diagram. There are no obstacles above, and these segments are allowed to move up as far as they want.

The three unsatisfied vars (1, 3, and 4) are noted in the notes column.

Constraints

I will not spell out all 23 constraints. I believe that most of them are fine, while the problematic ones are the following equality constraints:

v7 == v2
v7 == v4

v9 == v2
v9 == v4
v9 == v7

v11 == v2
v11 == v4
v11 == v7
v11 == v9

Each of these is one of the special case equality constraints noted earlier, and is generated here:

else if (!nudgeSharedPathsWithCommonEnd &&
(m_shared_path_connectors_with_common_endpoints.count(
UnsignedPair(currSegment->connRef->id(), prevSeg->connRef->id())) > 0))
{
// We don't want to nudge apart these two segments
// since they are from a shared path with a common
// endpoint. There might be multiple chains of
// segments that don't all have the same endpoints
// so we need to make this an equality to prevent
// some of them possibly getting nudged apart.
thisSepDist = 0;
equality = true;
}

Simplifying all these constraints, they amount to:

v2 == v4 == v7 == v9 == v11

Since v4 is supposed to be fixed, while v2 <= v3 and v3 is fixed at a position less than v4, the problem is unsatisfiable.

Because the problem is unsatifiable, and in particular because the right channel vars v1 and v3 are forced to move, the
nudgeOrthogonalRoutes() function asserts that these right channel vars have corresponding left channel vars. But they do not.

The assertion seems reasonable. It is reasonable to expect that, when a channel boundary is forced to move, it is because the channel is too narrow, there is not enough space for the free vars inside. And that requires that both sides of the channel are finite. But the present example shows a case where channel boundaries are forced to move for a very different reason, namely the special equality constraints generated in the if (!nudgeSharedPathsWithCommonEnd... clause.

Possible solution

In the example, it seems to me that the equality constraints are inappropriate unless the segments' desired positions are equal. So, v9 == v7 may make sense, but each of v2, v4, v11 should be allowed to differ, since they all have different desired positions.

In terms of the comments written in the code where these equality constraints are generated, it seems like we shouldn't be trying to "prevent" these segments from being "nudged apart." They already are apart, and we want them that way.

I wonder if these equality constraints were only really meant to keep together those segments that were put together during the justUnifying pass.

Should this be added as another condition? If so, the clause:

else if (!nudgeSharedPathsWithCommonEnd &&
        (m_shared_path_connectors_with_common_endpoints.count(
             UnsignedPair(currSegment->connRef->id(), prevSeg->connRef->id())) > 0))

would become:

else if (!nudgeSharedPathsWithCommonEnd &&
        (m_shared_path_connectors_with_common_endpoints.count(
             UnsignedPair(currSegment->connRef->id(), prevSeg->connRef->id())) > 0) &&
             currSegment->variable->desiredPosition == prevSeg->variable->desiredPosition)

To really judge whether this is the right solution would require a full grasp of all the cases these
equality constraints were designed to handle, and I don't have that grasp. I will open a PR to encode
this solution, as well as to provide the test case that generated the figures above, but I will mark
it as a draft, since I'm not sure if it's really the right solution or not.

@skieffer skieffer linked a pull request Sep 2, 2023 that will close this issue
@skieffer
Copy link
Collaborator Author

skieffer commented Sep 2, 2023

The proposed solution does produce a nice result:

with_proposed_soln

It is kind of a middle ground between the two figures in the original post. There is some nudging, while still honoring the
nudgeSharedPathsWithCommonEnd = false setting.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant