- The crank-and-piston mechanism from this old posting.
- The moving hockey stick from that posting.
- A simple rotating bar;
- and a rotating triangle, made from three bars. This leads to a lot of rigid-body-constraints that have to be solved.
[Test]
public void TestMovingHockeyStick() {
const string s =
@".config (20, 200, 200);
B : .bar P = [0,0] Q = [5,5] R = [5,30];
@0 B.P = [60 + .t, 60 + .t];
B.Q = [65 + .t, 65 + .t];
B.R = [65 + .t, 90 + .t];
@10";
Script script = Program.Parse(s);
string prefix = MkTestDir();
Program.Interpret(script, prefix);
}
public void TestMovingHockeyStick() {
const string s =
@".config (20, 200, 200);
B : .bar P = [0,0] Q = [5,5] R = [5,30];
@0 B.P = [60 + .t, 60 + .t];
B.Q = [65 + .t, 65 + .t];
B.R = [65 + .t, 90 + .t];
@10";
Script script = Program.Parse(s);
string prefix = MkTestDir();
Program.Interpret(script, prefix);
}
MkTestDir() is a simple method that creates a test output directory. This is not standard for a unit test, but as I explained here, I want to (also) test the results visually, and so I need the frames in a directory to assemble them into a movie.
Let us run the test ... and ... it is ... red: It throws an exception that says "System.Exception : No solution found for frame 1." What happened?
Let us think a moment: In each step, the solver takes the first open node and tries to match all constraints against the rewriting rules. But if no rule matches, it closes the node without an expansion, which means it is "unsolvable," and continues with the next open node. Missing a solution, therefore, means that
- either a rule that should have matched did not match (because of a programming error);
- or there is not yet a rule for some situation—our two rules are certainly not enough to solve all constraint systems!
We already have a readable representation of constraints, using our ToStringVisitor. With that code, we can output a single SolverNode:
public override string ToString() {
return "SolverNode"
+ _constraints.Aggregate("",
(s, c) => s + "\r\n ! " + c)
+ _variableInRangeKnowledges.Aggregate("",
(s, k) => s + "\r\n "
+ k.Key.Name
+ " = " + ((VariableValueRestriction)k.Value).Value);
}
return "SolverNode"
+ _constraints.Aggregate("",
(s, c) => s + "\r\n ! " + c)
+ _variableInRangeKnowledges.Aggregate("",
(s, k) => s + "\r\n "
+ k.Key.Name
+ " = " + ((VariableValueRestriction)k.Value).Value);
}
Which additional structures do we want to see?
- We want to see the "history" of a SolverNode: From which node it was crated ("expanded"), and how.
- In addition, we want to see all the nodes that did not result in a solution, i.e., that were expanded to an empty set: After all, one (or more) of them might contain constraints that should have been matched, but were somehow missed.
private static int _debugIdCount = 1000;
public readonly int DebugId = _debugIdCount++;
public readonly int DebugId = _debugIdCount++;
In addition, we remember the creating node, add a string about the creation rule, and write the same information recursively for a few predecessor nodes. To get a feeling for the node graph, I also want to see whether a SolverNode is the sole descendent of its predecessor, or whether there is more than one expansion. Here is a code snippet that tries to do this:
private readonly SolverNode _debugOrigin;
private int _debugSuccessors;
private string DebugCreationRule;
private string DebugOriginChain(int depth) {
return
(_debugSuccessors == 0 ? "" : _debugSuccessors == 1 ? "-" : "*") +
DebugId + "<" + DebugCreationRule +
(depth > 10 ? "..."
: _debugOrigin == null ? ""
: _debugOrigin.DebugOriginChain(depth + 1));
}
private int _debugSuccessors;
private string DebugCreationRule;
private string DebugOriginChain(int depth) {
return
(_debugSuccessors == 0 ? "" : _debugSuccessors == 1 ? "-" : "*") +
DebugId + "<" + DebugCreationRule +
(depth > 10 ? "..."
: _debugOrigin == null ? ""
: _debugOrigin.DebugOriginChain(depth + 1));
}
Now, when we call DebugOriginChain() inside ToString(), we now get output like the following:
{SolverNode 1020 < 0=C-1019 < 0=V+C-1018 < 0=C-1017 < 0=V+C-1016 < 0=C-1015 < 0=V+C... ! {EqualsZeroConstraint}0 = 60+-(65+root 25) ! {EqualsZeroConstraint}0 = 0+-(0+root 0) ! {EqualsZeroConstraint}0 = 60+-(90+root 900) ! {EqualsZeroConstraint}0 = 65+-(65+root 0) ! {EqualsZeroConstraint}0 = 65+-(90+root 625) B.P.X = 60 B.P.Y = 60 B.P.Z = 0 B.Q.X = 65 ...
The lines after the first one are, first, the constraints, and then the variable assignments. The first line can be read as follows:
- First, we are told that node id is 1020.
- Then, we see the predecessor nodes, and to the left of each, the name of the rule that created. For example, 1019 was created from 1018 by a 0=V+C rule rewrite, and the current node, 1020, was in turn created by a 0=C rewrite (I do not show the additional string we must pass into the rewrite rule, and a little bit of code that sets DebugCreationRule in Expand—it is very simple).
public static readonly List<SolverNode> DebugDeadNodes =
new List<SolverNode>();
public static IEnumerable<SolverNode> SolverStep(...) {
...
SolverNode selected = ...;
IEnumerable<SolverNode> expandedSets = selected.Expand(...);
if (!expandedSets.Any()) {
DebugDeadNodes.Add(selected);
}
...
}
new List<SolverNode>();
public static IEnumerable<SolverNode> SolverStep(...) {
...
SolverNode selected = ...;
IEnumerable<SolverNode> expandedSets = selected.Expand(...);
if (!expandedSets.Any()) {
DebugDeadNodes.Add(selected);
}
...
}
When the solver algorithm runs out of nodes without finding a solution, it writes all these nodes to the debug output so that we can find out what went wrong:
public static int Solve(...) {
...
do {
if (!open.Any()) {
Debug.WriteLine("---- " + DebugDeadNodes.Count
+ " dead nodes ----");
foreach (var deadNode in DebugDeadNodes) {
Debug.WriteLine(deadNode);
}
throw new Exception("No solution found for frame " + ...);
}
...
} while (...);
...
}
...
do {
if (!open.Any()) {
Debug.WriteLine("---- " + DebugDeadNodes.Count
+ " dead nodes ----");
foreach (var deadNode in DebugDeadNodes) {
Debug.WriteLine(deadNode);
}
throw new Exception("No solution found for frame " + ...);
}
...
} while (...);
...
}
Using this machinery, we can again run the test. And here is the output of the dead nodes:
---- 1 dead nodes ---- SolverNode 1020 < 0=C-1019 < 0=V+C-1018 < 0=C-1017 < 0=V+C... ! {EqualsZeroConstraint}0 = 60+-(65+root 25) ! {EqualsZeroConstraint}0 = 0+-(0+root 0) ! {EqualsZeroConstraint}0 = 60+-(90+root 900) ! {EqualsZeroConstraint}0 = 65+-(65+root 0) ! {EqualsZeroConstraint}0 = 65+-(90+root 625) B.P.X = 60 B.P.Y = 60 B.P.Z = 0 B.Q.X = 65 B.Q.Y = 65 B.Q.Z = 0 B.R.X = 65 B.R.Y = 90 B.R.Z = 0 ZERO = 0Let us think about this result in the next posting.
No comments:
Post a Comment