| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent f801618 commit b87bfca
2 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -291,13 +291,59 @@ bool System::SolveLeastSquares() { | |||
| 291 | 291 | } | |
| 292 | 292 | } | |
| 293 | 293 | ||
| 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; | ||
| 297 | 333 | ||
| 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; | ||
| 299 | 344 | ||
| 300 | - mat.X = mat.A.num.transpose() * z; | ||
| 345 | + mat.X = mat.A.num.transpose() * z; | ||
| 346 | + } | ||
| 301 | 347 | ||
| 302 | 348 | for(int c = 0; c < mat.n; c++) { | |
| 303 | 349 | mat.X[c] *= scale[c]; | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -4,15 +4,82 @@ | |||
| 4 | 4 | // Copyright 2017 whitequark | |
| 5 | 5 | //----------------------------------------------------------------------------- | |
| 6 | 6 | ||
| 7 | + #include "solvespace.h" | ||
| 7 | 8 | #include "expr.h" | |
| 8 | 9 | #include "platform/platform.h" | |
| 9 | 10 | ||
| 10 | 11 | using namespace SolveSpace; | |
| 11 | 12 | ||
| 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 | + | ||
| 12 | 75 | int main(int argc, char **argv) { | |
| 13 | 76 | std::vector<std::string> args = Platform::InitCli(argc, argv); | |
| 14 | 77 | ||
| 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") { | ||
| 16 | 83 | std::string expr = args[2], err; | |
| 17 | 84 | Expr *e = Expr::Parse(expr.c_str(), &err); | |
| 18 | 85 | if(e == NULL) { | |
@@ -28,6 +95,10 @@ int main(int argc, char **argv) { | |||
| 28 | 95 | Commands: | |
| 29 | 96 | expr [expr] | |
| 30 | 97 | 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. | ||
| 31 | 102 | )"); | |
| 32 | 103 | } | |
| 33 | 104 | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments