Solvespace
592 строки · 18.4 Кб
1//-----------------------------------------------------------------------------
2// Once we've written our constraint equations in the symbolic algebra system,
3// these routines linearize them, and solve by a modified Newton's method.
4// This also contains the routines to detect non-convergence or inconsistency,
5// and report diagnostics to the user.
6//
7// Copyright 2008-2013 Jonathan Westhues.
8//-----------------------------------------------------------------------------
9#include "solvespace.h"10
11#include <Eigen/Core>12#include <Eigen/SparseQR>13
14// The solver will converge all unknowns to within this tolerance. This must
15// always be much less than LENGTH_EPS, and in practice should be much less.
16const double System::CONVERGE_TOLERANCE = (LENGTH_EPS/(1e2));17
18constexpr size_t LikelyPartialCountPerEq = 10;19
20bool System::WriteJacobian(int tag) {21// Clear all22mat.param.clear();23mat.eq.clear();24mat.A.sym.setZero();25mat.B.sym.clear();26
27for(Param &p : param) {28if(p.tag != tag) continue;29mat.param.push_back(p.h);30}31mat.n = mat.param.size();32
33for(Equation &e : eq) {34if(e.tag != tag) continue;35mat.eq.push_back(&e);36}37mat.m = mat.eq.size();38mat.A.sym.resize(mat.m, mat.n);39mat.A.sym.reserve(Eigen::VectorXi::Constant(mat.n, LikelyPartialCountPerEq));40
41// Fill the param id to index map42std::map<uint32_t, int> paramToIndex;43for(int j = 0; j < mat.n; j++) {44paramToIndex[mat.param[j].v] = j;45}46
47if(mat.eq.size() >= MAX_UNKNOWNS) {48return false;49}50std::vector<hParam> paramsUsed;51// In some experimenting, this is almost always the right size.52// Value is usually between 0 and 20, comes from number of constraints?53mat.B.sym.reserve(mat.eq.size());54for(size_t i = 0; i < mat.eq.size(); i++) {55Equation *e = mat.eq[i];56if(e->tag != tag) continue;57// Simplify (fold) then deep-copy the current equation.58Expr *f = e->e->FoldConstants();59f = f->DeepCopyWithParamsAsPointers(¶m, &(SK.param));60
61paramsUsed.clear();62f->ParamsUsedList(¶msUsed);63
64for(hParam &p : paramsUsed) {65// Find the index of this parameter66auto it = paramToIndex.find(p.v);67if(it == paramToIndex.end()) continue;68// this is the parameter index69const int j = it->second;70// compute partial derivative of f71Expr *pd = f->PartialWrt(p);72pd = pd->FoldConstants();73if(pd->IsZeroConst())74continue;75mat.A.sym.insert(i, j) = pd;76}77paramsUsed.clear();78mat.B.sym.push_back(f);79}80return true;81}
82
83void System::EvalJacobian() {84using namespace Eigen;85mat.A.num.setZero();86mat.A.num.resize(mat.m, mat.n);87const int size = mat.A.sym.outerSize();88
89for(int k = 0; k < size; k++) {90for(SparseMatrix <Expr *>::InnerIterator it(mat.A.sym, k); it; ++it) {91double value = it.value()->Eval();92if(EXACT(value == 0.0)) continue;93mat.A.num.insert(it.row(), it.col()) = value;94}95}96mat.A.num.makeCompressed();97}
98
99bool System::IsDragged(hParam p) {100const auto b = dragged.begin();101const auto e = dragged.end();102return e != std::find(b, e, p);103}
104
105Param *System::GetLastParamSubstitution(Param *p) {106Param *current = p;107while(current->substd != NULL) {108current = current->substd;109if(current == p) {110// Break the loop111current->substd = NULL;112break;113}114}115return current;116}
117
118void System::SortSubstitutionByDragged(Param *p) {119std::vector<Param *> subsParams;120Param *by = NULL;121Param *current = p;122while(current != NULL) {123subsParams.push_back(current);124if(IsDragged(current->h)) {125by = current;126}127current = current->substd;128}129if(by == NULL) by = p;130for(Param *p : subsParams) {131if(p == by) continue;132p->substd = by;133p->tag = VAR_SUBSTITUTED;134}135by->substd = NULL;136by->tag = 0;137}
138
139void System::SubstituteParamsByLast(Expr *e) {140ssassert(e->op != Expr::Op::PARAM_PTR, "Expected an expression that refer to params via handles");141
142if(e->op == Expr::Op::PARAM) {143Param *p = param.FindByIdNoOops(e->parh);144if(p != NULL) {145Param *s = GetLastParamSubstitution(p);146if(s != NULL) {147e->parh = s->h;148}149}150} else {151int c = e->Children();152if(c >= 1) {153SubstituteParamsByLast(e->a);154if(c >= 2) SubstituteParamsByLast(e->b);155}156}157}
158
159void System::SolveBySubstitution() {160for(auto &teq : eq) {161Expr *tex = teq.e;162
163if(tex->op == Expr::Op::MINUS &&164tex->a->op == Expr::Op::PARAM &&165tex->b->op == Expr::Op::PARAM)166{167hParam a = tex->a->parh;168hParam b = tex->b->parh;169if(!(param.FindByIdNoOops(a) && param.FindByIdNoOops(b))) {170// Don't substitute unless they're both solver params;171// otherwise it's an equation that can be solved immediately,172// or an error to flag later.173continue;174}175
176if(a.v == b.v) {177teq.tag = EQ_SUBSTITUTED;178continue;179}180
181Param *pa = param.FindById(a);182Param *pb = param.FindById(b);183
184// Take the last substitution of parameter a185// This resulted in creation of substitution chains186Param *last = GetLastParamSubstitution(pa);187last->substd = pb;188last->tag = VAR_SUBSTITUTED;189
190if(pb->substd != NULL) {191// Break the loops192GetLastParamSubstitution(pb);193// if b loop was broken194if(pb->substd == NULL) {195// Clear substitution196pb->tag = 0;197}198}199teq.tag = EQ_SUBSTITUTED;200}201}202
203//204for(Param &p : param) {205SortSubstitutionByDragged(&p);206}207
208// Substitute all the equations209for(auto &req : eq) {210SubstituteParamsByLast(req.e);211}212
213// Substitute all the parameters with last substitutions214for(auto &p : param) {215if(p.substd == NULL) continue;216p.substd = GetLastParamSubstitution(p.substd);217}218}
219
220//-----------------------------------------------------------------------------
221// Calculate the rank of the Jacobian matrix
222//-----------------------------------------------------------------------------
223int System::CalculateRank() {224using namespace Eigen;225if(mat.n == 0 || mat.m == 0) return 0;226SparseQR <SparseMatrix<double>, COLAMDOrdering<int>> solver;227solver.compute(mat.A.num);228int result = solver.rank();229return result;230}
231
232bool System::TestRank(int *dof) {233EvalJacobian();234int jacobianRank = CalculateRank();235// We are calculating dof based on real rank, not mat.m.236// Using this approach we can calculate real dof even when redundant is allowed.237if(dof != NULL) *dof = mat.n - jacobianRank;238return jacobianRank == mat.m;239}
240
241bool System::SolveLinearSystem(const Eigen::SparseMatrix <double> &A,242const Eigen::VectorXd &B, Eigen::VectorXd *X)243{
244if(A.outerSize() == 0) return true;245using namespace Eigen;246SparseQR<SparseMatrix<double>, COLAMDOrdering<int>> solver;247//SimplicialLDLT<SparseMatrix<double>> solver;248solver.compute(A);249*X = solver.solve(B);250return (solver.info() == Success);251}
252
253bool System::SolveLeastSquares() {254using namespace Eigen;255// Scale the columns; this scale weights the parameters for the least256// squares solve, so that we can encourage the solver to make bigger257// changes in some parameters, and smaller in others.258mat.scale = VectorXd::Ones(mat.n);259for(int c = 0; c < mat.n; c++) {260if(IsDragged(mat.param[c])) {261// It's least squares, so this parameter doesn't need to be all262// that big to get a large effect.263mat.scale[c] = 1 / 20.0;264}265}266
267const int size = mat.A.num.outerSize();268for(int k = 0; k < size; k++) {269for(SparseMatrix<double>::InnerIterator it(mat.A.num, k); it; ++it) {270it.valueRef() *= mat.scale[it.col()];271}272}273
274SparseMatrix<double> AAt = mat.A.num * mat.A.num.transpose();275AAt.makeCompressed();276VectorXd z(mat.n);277
278if(!SolveLinearSystem(AAt, mat.B.num, &z)) return false;279
280mat.X = mat.A.num.transpose() * z;281
282for(int c = 0; c < mat.n; c++) {283mat.X[c] *= mat.scale[c];284}285return true;286}
287
288bool System::NewtonSolve(int tag) {289
290int iter = 0;291bool converged = false;292int i;293
294// Evaluate the functions at our operating point.295mat.B.num = Eigen::VectorXd(mat.m);296for(i = 0; i < mat.m; i++) {297mat.B.num[i] = (mat.B.sym[i])->Eval();298}299do {300// And evaluate the Jacobian at our initial operating point.301EvalJacobian();302
303if(!SolveLeastSquares()) break;304
305// Take the Newton step;306// J(x_n) (x_{n+1} - x_n) = 0 - F(x_n)307for(i = 0; i < mat.n; i++) {308Param *p = param.FindById(mat.param[i]);309p->val -= mat.X[i];310if(IsReasonable(p->val)) {311// Very bad, and clearly not convergent312return false;313}314}315
316// Re-evalute the functions, since the params have just changed.317for(i = 0; i < mat.m; i++) {318mat.B.num[i] = (mat.B.sym[i])->Eval();319}320// Check for convergence321converged = true;322for(i = 0; i < mat.m; i++) {323if(IsReasonable(mat.B.num[i])) {324return false;325}326if(fabs(mat.B.num[i]) > CONVERGE_TOLERANCE) {327converged = false;328break;329}330}331} while(iter++ < 50 && !converged);332
333return converged;334}
335
336void System::WriteEquationsExceptFor(hConstraint hc, Group *g) {337// Generate all the equations from constraints in this group338for(auto &con : SK.constraint) {339ConstraintBase *c = &con;340if(c->group != g->h) continue;341if(c->h == hc) continue;342
343if(c->HasLabel() && c->type != Constraint::Type::COMMENT &&344g->allDimsReference)345{346// When all dimensions are reference, we adjust them to display347// the correct value, and then don't generate any equations.348c->ModifyToSatisfy();349continue;350}351if(g->relaxConstraints && c->type != Constraint::Type::POINTS_COINCIDENT) {352// When the constraints are relaxed, we keep only the point-353// coincident constraints, and the constraints generated by354// the entities and groups.355continue;356}357
358c->GenerateEquations(&eq);359}360// And the equations from entities361for(auto &ent : SK.entity) {362EntityBase *e = &ent;363if(e->group != g->h) continue;364
365e->GenerateEquations(&eq);366}367// And from the groups themselves368g->GenerateEquations(&eq);369}
370
371void System::FindWhichToRemoveToFixJacobian(Group *g, List<hConstraint> *bad, bool forceDofCheck) {372auto time = GetMilliseconds();373g->solved.timeout = false;374int a;375
376for(a = 0; a < 2; a++) {377for(auto &con : SK.constraint) {378if((GetMilliseconds() - time) > g->solved.findToFixTimeout) {379g->solved.timeout = true;380return;381}382
383ConstraintBase *c = &con;384if(c->group != g->h) continue;385if((c->type == Constraint::Type::POINTS_COINCIDENT && a == 0) ||386(c->type != Constraint::Type::POINTS_COINCIDENT && a == 1))387{388// Do the constraints in two passes: first everything but389// the point-coincident constraints, then only those390// constraints (so they appear last in the list).391continue;392}393
394param.ClearTags();395eq.Clear();396WriteEquationsExceptFor(c->h, g);397eq.ClearTags();398
399// It's a major speedup to solve the easy ones by substitution here,400// and that doesn't break anything.401if(!forceDofCheck) {402SolveBySubstitution();403}404
405WriteJacobian(0);406EvalJacobian();407
408int rank = CalculateRank();409if(rank == mat.m) {410// We fixed it by removing this constraint411bad->Add(&(c->h));412}413}414}415}
416
417SolveResult System::Solve(Group *g, int *rank, int *dof, List<hConstraint> *bad,418bool andFindBad, bool andFindFree, bool forceDofCheck)419{
420WriteEquationsExceptFor(Constraint::NO_CONSTRAINT, g);421
422bool rankOk;423
424/*
425int x;
426dbp("%d equations", eq.n);
427for(x = 0; x < eq.n; x++) {
428dbp(" %.3f = %s = 0", eq[x].e->Eval(), eq[x].e->Print());
429}
430dbp("%d parameters", param.n);
431for(x = 0; x < param.n; x++) {
432dbp(" param %08x at %.3f", param[x].h.v, param[x].val);
433} */
434
435// All params and equations are assigned to group zero.436param.ClearTags();437eq.ClearTags();438
439// Since we are suppressing dof calculation or allowing redundant, we440// can't / don't want to catch result of dof checking without substitution441if(g->suppressDofCalculation || g->allowRedundant || !forceDofCheck) {442SolveBySubstitution();443}444
445// Before solving the big system, see if we can find any equations that446// are soluble alone. This can be a huge speedup. We don't know whether447// the system is consistent yet, but if it isn't then we'll catch that448// later.449int alone = 1;450for(auto &e : eq) {451if(e.tag != 0)452continue;453
454hParam hp = e.e->ReferencedParams(¶m);455if(hp == Expr::NO_PARAMS) continue;456if(hp == Expr::MULTIPLE_PARAMS) continue;457
458Param *p = param.FindById(hp);459if(p->tag != 0) continue; // let rank test catch inconsistency460
461e.tag = alone;462p->tag = alone;463WriteJacobian(alone);464if(!NewtonSolve(alone)) {465// We don't do the rank test, so let's arbitrarily return466// the DIDNT_CONVERGE result here.467rankOk = true;468// Failed to converge, bail out early469goto didnt_converge;470}471alone++;472}473
474// Now write the Jacobian for what's left, and do a rank test; that475// tells us if the system is inconsistently constrained.476if(!WriteJacobian(0)) {477return SolveResult::TOO_MANY_UNKNOWNS;478}479// Clear dof value in order to have indication when dof is actually not calculated480if(dof != NULL) *dof = -1;481// We are suppressing or allowing redundant, so we no need to catch unsolveable + redundant482rankOk = (!g->suppressDofCalculation && !g->allowRedundant) ? TestRank(dof) : true;483
484// And do the leftovers as one big system485if(!NewtonSolve(0)) {486goto didnt_converge;487}488
489// Here we are want to calculate dof even when redundant is allowed, so just handle suppressing490rankOk = (!g->suppressDofCalculation) ? TestRank(dof) : true;491if(!rankOk) {492if(andFindBad) FindWhichToRemoveToFixJacobian(g, bad, forceDofCheck);493} else {494MarkParamsFree(andFindFree);495}496// System solved correctly, so write the new values back in to the497// main parameter table.498for(auto &p : param) {499double val;500if(p.tag == VAR_SUBSTITUTED) {501val = p.substd->val;502} else {503val = p.val;504}505Param *pp = SK.GetParam(p.h);506pp->val = val;507pp->known = true;508pp->free = p.free;509}510return rankOk ? SolveResult::OKAY : SolveResult::REDUNDANT_OKAY;511
512didnt_converge:513SK.constraint.ClearTags();514// Not using range-for here because index is used in additional ways515for(size_t i = 0; i < mat.eq.size(); i++) {516if(fabs(mat.B.num[i]) > CONVERGE_TOLERANCE || IsReasonable(mat.B.num[i])) {517// This constraint is unsatisfied.518if(!mat.eq[i]->h.isFromConstraint()) continue;519
520hConstraint hc = mat.eq[i]->h.constraint();521ConstraintBase *c = SK.constraint.FindByIdNoOops(hc);522if(!c) continue;523// Don't double-show constraints that generated multiple524// unsatisfied equations525if(!c->tag) {526bad->Add(&(c->h));527c->tag = 1;528}529}530}531
532return rankOk ? SolveResult::DIDNT_CONVERGE : SolveResult::REDUNDANT_DIDNT_CONVERGE;533}
534
535SolveResult System::SolveRank(Group *g, int *rank, int *dof, List<hConstraint> *bad,536bool andFindBad, bool andFindFree)537{
538WriteEquationsExceptFor(Constraint::NO_CONSTRAINT, g);539
540// All params and equations are assigned to group zero.541param.ClearTags();542eq.ClearTags();543
544// Now write the Jacobian, and do a rank test; that545// tells us if the system is inconsistently constrained.546if(!WriteJacobian(0)) {547return SolveResult::TOO_MANY_UNKNOWNS;548}549
550bool rankOk = TestRank(dof);551if(!rankOk) {552// When we are testing with redundant allowed, we don't want to have additional info553// about redundants since this test is working only for single redundant constraint554if(!g->suppressDofCalculation && !g->allowRedundant) {555if(andFindBad) FindWhichToRemoveToFixJacobian(g, bad, true);556}557} else {558MarkParamsFree(andFindFree);559}560return rankOk ? SolveResult::OKAY : SolveResult::REDUNDANT_OKAY;561}
562
563void System::Clear() {564entity.Clear();565param.Clear();566eq.Clear();567dragged.Clear();568mat.A.num.setZero();569mat.A.sym.setZero();570}
571
572void System::MarkParamsFree(bool find) {573// If requested, find all the free (unbound) variables. This might be574// more than the number of degrees of freedom. Don't always do this,575// because the display would get annoying and it's slow.576for(auto &p : param) {577p.free = false;578
579if(find) {580if(p.tag == 0) {581p.tag = VAR_DOF_TEST;582WriteJacobian(0);583EvalJacobian();584int rank = CalculateRank();585if(rank == mat.m) {586p.free = true;587}588p.tag = 0;589}590}591}592}
593
594