FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Solve the Newton step without forming the normal equations. · solvespace/solvespace@b87bfca · GitHub

Commit b87bfca

Browse files
authored andcommitted
Solve the Newton step without forming the normal equations.
SolveLeastSquares() computed the minimum norm Newton step as x = A'*(A*A')^-1*B, forming A*A' explicitly and factoring it with a rank-revealing sparse QR. That squares the condition number of the Jacobian. Our equations mix dimensionless quantities with lengths and with areas, so the spread of magnitudes in A already grows with the physical size of the sketch; squaring it pushes the smallest pivot of A*A' below the threshold Eigen uses to call a column linearly dependent, which is proportional to the largest column norm. Eigen's rank-truncated solve then silently zeroes that component of the step, so the residual of one equation can never be driven to zero, Newton's method stalls, and a perfectly solvable sketch is reported as having incompatible constraints. On the file from the bug report, two 4 m lines constrained perpendicular with a point on line, the transition is exact: with the second line pinned at 4 m and the first at 2880 mm, the fifth pivot of A*A' is 3.657e-7 against a threshold of 3.6557e-7 and the sketch solves; at 2881 mm the pivot falls just below the threshold, the rank drops from 5 to 4, and the solve fails, with the step norm collapsing to 1e-13 while one residual stays pinned at 0.019. Factor A' = Q*R directly instead. Then A*A' = P*R'*R*P', so the same z = (A*A')^-1*B comes from two triangular solves against R, and the rank decision is taken on pivots that scale like A rather than like A*A'. In that same sweep, the longest first line that solves goes from 2.88 m to 76.9 km. Rank determination for redundant constraints is untouched; it runs in TestRank(), against A itself. Over-constrained sketches are in fact reported better than before: duplicating a constraint in that file used to be reported as redundant below about 3 m but as unsolvable above it, and is now reported as redundant at every size, out to 100 m. The wide case, more equations than unknowns, keeps using the normal equations, since Eigen's sparse QR wants a matrix that is at least as tall as it is wide. Such a system is redundant anyway, and it gives the same result as before. A system with no equations or no unknowns now returns a zero step, rather than multiplying an uninitialized vector by a matrix of a mismatched size. Also teach the debug tool to load a file and report how each group solved, and to load a file and save it back, which is how a linked part gets re-solved without a GUI. Fixes #1354, both the original report and mesr's assembly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent f801618 commit b87bfca

2 files changed

Lines changed: 123 additions & 6 deletions

File tree

‎src/system.cpp‎

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -291,13 +291,59 @@ bool System::SolveLeastSquares() {
291291
}
292292
}
293293

294-
SparseMatrix<double> AAt = mat.A.num * mat.A.num.transpose();
295-
AAt.makeCompressed();
296-
VectorXd z(mat.n);
294+
if(mat.m == 0 || mat.n == 0) {
295+
mat.X = VectorXd::Zero(mat.n);
296+
return true;
297+
}
298+
299+
if(mat.m <= mat.n) {
300+
// We want the minimum norm solution of the underdetermined system
301+
// A*x = B, which is x = A'*(A*A')^-1*B. Forming the normal equations
302+
// A*A' explicitly squares the condition number of A; since our
303+
// Jacobian mixes dimensionless quantities with lengths, areas and
304+
// volumes, its condition number already grows with the size of the
305+
// sketch, and squaring it is what makes large models fail. Eigen's
306+
// rank-revealing QR then drops pivots below a threshold proportional
307+
// to the largest column norm, and its rank-truncated solve silently
308+
// zeroes the corresponding component of the Newton step, so the
309+
// residual of one equation can never be driven to zero and we report
310+
// unsolvable constraints for a perfectly solvable sketch.
311+
//
312+
// Factor A' instead, as A'*P = Q*R. Then A*A' = P*R'*R*P', so
313+
// z = (A*A')^-1*B = P*R^-1*R'^-1*P'*B,
314+
// which is two triangular solves, and the rank decision is taken on
315+
// R (whose pivots scale like A, not like A*A').
316+
SparseMatrix<double> At = mat.A.num.transpose();
317+
At.makeCompressed();
318+
319+
SparseQR<SparseMatrix<double>, COLAMDOrdering<int>> solver;
320+
solver.compute(At);
321+
if(solver.info() != Success) return false;
322+
323+
const int rank = (int)solver.rank();
324+
VectorXd rhs = solver.colsPermutation().transpose() * mat.B.num;
325+
VectorXd v = VectorXd::Zero(mat.m);
326+
if(rank > 0) {
327+
SparseMatrix<double> R = solver.matrixR().topLeftCorner(rank, rank);
328+
VectorXd w = R.triangularView<Upper>().transpose()
329+
.solve(rhs.topRows(rank));
330+
v.topRows(rank) = R.triangularView<Upper>().solve(w);
331+
}
332+
VectorXd z = solver.colsPermutation() * v;
297333

298-
if(!SolveLinearSystem(AAt, mat.B.num, &z)) return false;
334+
mat.X = mat.A.num.transpose() * z;
335+
} else {
336+
// More equations than unknowns; A' is wide, which Eigen's sparse QR
337+
// does not handle, so fall back to the normal equations. A system
338+
// like that is redundant anyway, and gets reported as such.
339+
SparseMatrix<double> AAt = mat.A.num * mat.A.num.transpose();
340+
AAt.makeCompressed();
341+
VectorXd z(mat.m);
342+
343+
if(!SolveLinearSystem(AAt, mat.B.num, &z)) return false;
299344

300-
mat.X = mat.A.num.transpose() * z;
345+
mat.X = mat.A.num.transpose() * z;
346+
}
301347

302348
for(int c = 0; c < mat.n; c++) {
303349
mat.X[c] *= scale[c];

‎test/debugtool.cpp‎

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,82 @@
44
// Copyright 2017 whitequark
55
//-----------------------------------------------------------------------------
66

7+
#include "solvespace.h"
78
#include "expr.h"
89
#include "platform/platform.h"
910

1011
using namespace SolveSpace;
1112

13+
static const char *SolveResultName(SolveResult how) {
14+
switch(how) {
15+
case SolveResult::OKAY: return "OKAY";
16+
case SolveResult::DIDNT_CONVERGE: return "DIDNT_CONVERGE";
17+
case SolveResult::REDUNDANT_OKAY: return "REDUNDANT_OKAY";
18+
case SolveResult::REDUNDANT_DIDNT_CONVERGE: return "REDUNDANT_DIDNT_CONVERGE";
19+
case SolveResult::TOO_MANY_UNKNOWNS: return "TOO_MANY_UNKNOWNS";
20+
}
21+
return "?";
22+
}
23+
24+
// Load a file, regenerate it, and report how every group solved. Useful to
25+
// reproduce solver failures without a GUI.
26+
static int CmdSolve(const std::string &filename) {
27+
SS.Init();
28+
SS.showToolbar = false;
29+
SS.checkClosedContour = false;
30+
31+
if(!SS.LoadFromFile(Platform::Path::From(filename))) {
32+
fprintf(stderr, "cannot load: %s\n", filename.c_str());
33+
return 1;
34+
}
35+
SS.AfterNewFile();
36+
37+
int failed = 0;
38+
for(Group &g : SK.group) {
39+
bool ok = g.IsSolvedOkay();
40+
if(!ok) failed++;
41+
fprintf(stderr, "group %08x %-24s %-24s dof=%d%s\n", g.h.v,
42+
g.DescriptionString().c_str(), SolveResultName(g.solved.how),
43+
g.solved.dof, ok ? "" : " FAILED");
44+
for(int i = 0; i < g.solved.remove.n; i++) {
45+
Constraint *c = SK.constraint.FindByIdNoOops(g.solved.remove[i]);
46+
if(!c) continue;
47+
fprintf(stderr, " bad constraint %08x %s\n", c->h.v,
48+
c->DescriptionString().c_str());
49+
}
50+
}
51+
fprintf(stderr, "%s: %d group(s) failed to solve\n", filename.c_str(), failed);
52+
return failed == 0 ? 0 : 1;
53+
}
54+
55+
// Load a file, regenerate it, and write it back out. This is what happens when
56+
// a part that other files link to is opened, edited and saved again.
57+
static int CmdResave(const std::string &filename) {
58+
SS.Init();
59+
SS.showToolbar = false;
60+
SS.checkClosedContour = false;
61+
62+
Platform::Path path = Platform::Path::From(filename);
63+
if(!SS.LoadFromFile(path)) {
64+
fprintf(stderr, "cannot load: %s\n", filename.c_str());
65+
return 1;
66+
}
67+
SS.AfterNewFile();
68+
if(!SS.SaveToFile(path)) {
69+
fprintf(stderr, "cannot save: %s\n", filename.c_str());
70+
return 1;
71+
}
72+
return 0;
73+
}
74+
1275
int main(int argc, char **argv) {
1376
std::vector<std::string> args = Platform::InitCli(argc, argv);
1477

15-
if(args.size() == 3 && args[1] == "expr") {
78+
if(args.size() == 3 && args[1] == "solve") {
79+
return CmdSolve(args[2]);
80+
} else if(args.size() == 3 && args[1] == "resave") {
81+
return CmdResave(args[2]);
82+
} else if(args.size() == 3 && args[1] == "expr") {
1683
std::string expr = args[2], err;
1784
Expr *e = Expr::Parse(expr.c_str(), &err);
1885
if(e == NULL) {
@@ -28,6 +95,10 @@ int main(int argc, char **argv) {
2895
Commands:
2996
expr [expr]
3097
Evaluate an expression.
98+
solve [file.slvs]
99+
Load a file and report how each group solved.
100+
resave [file.slvs]
101+
Load a file, regenerate it, and save it back.
31102
)");
32103
}
33104

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL