Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,15 @@ bool GenericConstraintSolver::buildSystem(const core::ConstraintParams *cParams,
// suppress the constraints that are on DOFS currently concerned by projective constraint
applyProjectiveConstraintOnConstraintMatrix(cParams);

// Get constraint info for hot-start BEFORE clear() resets the constraint count check
preClearCorrection(cParams);

Comment on lines +189 to +191
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get why this is needed. When you look at the implementation of the doPreClear, clearing the curren_cp wouldn't change a thing in what MechanicalGetConstraintInfoVisitor will get. So for me the pre/post is not necessary. could you try mergin both and calling the merged method after the clear ?

//clear and/or resize based on the number of constraints
current_cp->clear(numConstraints);

// Restore initial guess from previous timestep AFTER clear() zeros the forces
postClearCorrection();

getConstraintViolation(cParams, &current_cp->dFree);

{
Expand Down Expand Up @@ -380,6 +386,8 @@ void GenericConstraintSolver::storeConstraintLambdas(const core::ConstraintParam

bool GenericConstraintSolver::applyCorrection(const core::ConstraintParams *cParams, MultiVecId res1, MultiVecId res2)
{
doPreApplyCorrection();

computeAndApplyMotionCorrection(cParams, res1, res2);
storeConstraintLambdas(cParams);

Expand Down Expand Up @@ -440,7 +448,4 @@ void GenericConstraintSolver::addRegularization(linearalgebra::BaseMatrix& W, co
}
}




} //namespace sofa::component::constraint::lagrangian::solver
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ class SOFA_COMPONENT_CONSTRAINT_LAGRANGIAN_SOLVER_API GenericConstraintSolver :
sofa::core::MultiVecDerivId m_dxId;

virtual void initializeConstraintProblems();
virtual void preApplyCorrection() { doPreApplyCorrection(); }
void preClearCorrection(const core::ConstraintParams* cparams) { doPreClearCorrection(cparams); }
void postClearCorrection() { doPostClearCorrection(); }

/*****
*
Expand All @@ -122,11 +125,13 @@ class SOFA_COMPONENT_CONSTRAINT_LAGRANGIAN_SOLVER_API GenericConstraintSolver :
*
*/
virtual void doSolve( GenericConstraintProblem * problem, SReal timeout = 0.0) = 0;

virtual void doPreApplyCorrection() { }
virtual void doPreClearCorrection(const core::ConstraintParams* cparams) { SOFA_UNUSED(cparams); }
virtual void doPostClearCorrection() { }

virtual void addRegularization(linearalgebra::BaseMatrix& W, const SReal regularization);



private:

sofa::type::vector<core::behavior::BaseConstraintCorrection*> filteredConstraintCorrections() const;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,18 @@
#include <sofa/component/constraint/lagrangian/solver/UnbuiltConstraintProblem.h>
#include <sofa/component/constraint/lagrangian/solver/GenericConstraintSolver.h>

#include <sofa/helper/ScopedAdvancedTimer.h>

namespace sofa::component::constraint::lagrangian::solver
{

UnbuiltConstraintSolver::UnbuiltConstraintSolver()
: GenericConstraintSolver()
, d_initialGuess(initData(&d_initialGuess, false, "initialGuess", "Activate constraint force history to possibly improve convergence (hot start)."))
{

}

void UnbuiltConstraintSolver::doBuildSystem( const core::ConstraintParams *cParams, GenericConstraintProblem * problem, unsigned int numConstraints)
{
SOFA_UNUSED(cParams);
Expand All @@ -37,13 +46,14 @@ void UnbuiltConstraintSolver::doBuildSystem( const core::ConstraintParams *cPara
return;
}

// Initialize constraint sequence ONCE before iterating over constraint corrections
c_current_cp->constraints_sequence.resize(numConstraints);
std::iota(c_current_cp->constraints_sequence.begin(), c_current_cp->constraints_sequence.end(), 0);

for (const auto& cc : l_constraintCorrections)
{
if (!cc->isActive()) continue;

c_current_cp->constraints_sequence.resize(numConstraints);
std::iota(c_current_cp->constraints_sequence.begin(), c_current_cp->constraints_sequence.end(), 0);

// some constraint corrections (e.g LinearSolverConstraintCorrection)
// can change the order of the constraints, to optimize later computations
cc->resetForUnbuiltResolution(c_current_cp->getF(), c_current_cp->constraints_sequence);
Expand All @@ -59,11 +69,9 @@ void UnbuiltConstraintSolver::doBuildSystem( const core::ConstraintParams *cPara
for (unsigned int i = 0; i < numConstraints; i++)
c_current_cp->cclist_elems[i].resize(nbCC, nullptr);

unsigned int nbObjects = 0;
for (unsigned int c_id = 0; c_id < numConstraints;)
{
bool foundCC = false;
nbObjects++;
const unsigned int l = c_current_cp->constraintsResolutions[c_id]->getNbLines();

for (unsigned int j = 0; j < l_constraintCorrections.size(); j++)
Expand Down Expand Up @@ -93,6 +101,24 @@ void UnbuiltConstraintSolver::doBuildSystem( const core::ConstraintParams *cPara

}

void UnbuiltConstraintSolver::doPreApplyCorrection()
{
if (!d_initialGuess.getValue())
return;

// Save forces for hot-start in next timestep
keepContactForcesValue();
}
void UnbuiltConstraintSolver::doPreClearCorrection(const core::ConstraintParams* cparams)
{
getConstraintInfo(cparams);
}

void UnbuiltConstraintSolver::doPostClearCorrection()
{
computeInitialGuess();
}

void UnbuiltConstraintSolver::initializeConstraintProblems()
{
for (unsigned i=0; i< CP_BUFFER_SIZE; ++i)
Expand All @@ -102,5 +128,107 @@ void UnbuiltConstraintSolver::initializeConstraintProblems()
current_cp = m_cpBuffer[0].get();
}

void UnbuiltConstraintSolver::getConstraintInfo(const core::ConstraintParams* cparams)
{
if (d_initialGuess.getValue() && (m_numConstraints != 0))
{
SCOPED_TIMER("GetConstraintInfo");
m_constraintBlockInfo.clear();
m_constraintIds.clear();
simulation::mechanicalvisitor::MechanicalGetConstraintInfoVisitor(cparams, m_constraintBlockInfo, m_constraintIds).execute(getContext());
}
}

void UnbuiltConstraintSolver::computeInitialGuess()
{
if (!d_initialGuess.getValue() || m_numConstraints == 0)
return;

SCOPED_TIMER("InitialGuess");

SReal* force = current_cp->getF();
const int numConstraints = current_cp->getDimension();

// First, zero all forces
for (int c = 0; c < numConstraints; c++)
{
force[c] = 0.0;
}

// Then restore forces from previous timestep for matching persistent IDs
for (const ConstraintBlockInfo& info : m_constraintBlockInfo)
{
if (!info.parent) continue;
if (!info.hasId) continue;

auto previt = m_previousConstraints.find(info.parent);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the parent as a key is somewhat strange because it it irrelevant @the level of the solver. What is relevant is the element on which the force is applied. This information is only kept in the constraint jacobian matrix. At this level I don't think it is possible to get an actual proper hotstart. What is done here it is looking at the id of the constraint at the level of the component which transform the collision detection output into the constraint jacobian. So what you are comparing here is the place of the force in the original vector of the collision detection. So for me this hotstart is just as good as copying back the content of the old force vector into the new one while clamping or padding if size mismatch. This is only by chance that you get forces that are good because the collision detection is somewhat deterministic.

The only strong difference would be when there are two component responsible of transforming this collision output into contact force, this would keep the force at least in the good part of the vector (the old forces will be used a starting point of forces coming from the same collision detection), but there is no garantee that the force is applied in the same region of the object as before. This happens when you have multiple objects colliding. But really, I don't know how much you gain with respect of a simple copy/clipping/padding work.

Could you try out your example with this simple implementation to see if you get worst convergence ?

if (previt == m_previousConstraints.end()) continue;

const ConstraintBlockBuf& buf = previt->second;
const int c0 = info.const0;
const int nbl = (info.nbLines < buf.nbLines) ? info.nbLines : buf.nbLines;

for (int c = 0; c < info.nbGroups; ++c)
{
auto it = buf.persistentToConstraintIdMap.find(m_constraintIds[info.offsetId + c]);
if (it == buf.persistentToConstraintIdMap.end()) continue;

const int prevIndex = it->second;
if (prevIndex >= 0 && prevIndex + nbl <= static_cast<int>(m_previousForces.size()))
{
for (int l = 0; l < nbl; ++l)
{
force[c0 + c * nbl + l] = m_previousForces[prevIndex + l];
}
}
}
}
}

void UnbuiltConstraintSolver::keepContactForcesValue()
{
SCOPED_TIMER("KeepForces");

const SReal* force = current_cp->getF();
const unsigned int numConstraints = current_cp->getDimension();

// Store current forces
m_previousForces.resize(numConstraints);
for (unsigned int c = 0; c < numConstraints; ++c)
{
m_previousForces[c] = force[c];
}

// Clear previous history (mark all as invalid)
for (auto& previousConstraint : m_previousConstraints)
{
ConstraintBlockBuf& buf = previousConstraint.second;
for (auto& it2 : buf.persistentToConstraintIdMap)
{
it2.second = -1;
}
}
Comment on lines +203 to +210
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be better to simply clear the whole map and start fresh ? Otherwise if the parent keep changing, this map will grow without getting properly cleaned


// Fill info from current constraint IDs
for (const ConstraintBlockInfo& info : m_constraintBlockInfo)
{
if (!info.parent) continue;
if (!info.hasId) continue;

ConstraintBlockBuf& buf = m_previousConstraints[info.parent];
buf.nbLines = info.nbLines;

for (int c = 0; c < info.nbGroups; ++c)
{
buf.persistentToConstraintIdMap[m_constraintIds[info.offsetId + c]] = info.const0 + c * info.nbLines;
}
}

// Update constraint count for next iteration
m_numConstraints = numConstraints;
}




}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include <sofa/component/constraint/lagrangian/solver/GenericConstraintSolver.h>
#include <sofa/core/behavior/ConstraintResolution.h>

#include <sofa/simulation/mechanicalvisitor/MechanicalGetConstraintInfoVisitor.h>


namespace sofa::component::constraint::lagrangian::solver
Expand All @@ -40,11 +41,50 @@ class SOFA_COMPONENT_CONSTRAINT_LAGRANGIAN_SOLVER_API UnbuiltConstraintSolver :
{
public:
SOFA_CLASS(UnbuiltConstraintSolver, GenericConstraintSolver);

protected:
UnbuiltConstraintSolver();


public:
virtual void initializeConstraintProblems() override;

Data<bool> d_initialGuess; ///< Activate constraint force history to improve convergence (hot start)

protected:
virtual void doBuildSystem( const core::ConstraintParams *cParams, GenericConstraintProblem * problem, unsigned int numConstraints) override;

virtual void doPreApplyCorrection() override;
virtual void doPreClearCorrection(const core::ConstraintParams* cparams) override;
virtual void doPostClearCorrection() override;

///<
// Hot-start mechanism types
typedef core::behavior::BaseLagrangianConstraint::ConstraintBlockInfo ConstraintBlockInfo;
typedef core::behavior::BaseLagrangianConstraint::PersistentID PersistentID;
typedef core::behavior::BaseLagrangianConstraint::VecConstraintBlockInfo VecConstraintBlockInfo;
typedef core::behavior::BaseLagrangianConstraint::VecPersistentID VecPersistentID;

class ConstraintBlockBuf
{
public:
std::map<PersistentID, int> persistentToConstraintIdMap;
int nbLines{0}; ///< how many dofs (i.e. lines in the matrix) are used by each constraint
};

/// Compute initial guess for constraint forces from previous timestep
void computeInitialGuess();

/// Save constraint forces for use as initial guess in next timestep
void keepContactForcesValue();

/// Get constraint info (block info and persistent IDs) for hot-start
void getConstraintInfo(const core::ConstraintParams* cparams);

// Hot-start data storage
std::map<core::behavior::BaseLagrangianConstraint*, ConstraintBlockBuf> m_previousConstraints;
type::vector<SReal> m_previousForces;
VecConstraintBlockInfo m_constraintBlockInfo;
VecPersistentID m_constraintIds;
unsigned int m_numConstraints{0}; ///< Number of constraints from current/previous timestep
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you prefix all of these by previous so it is clear that they are used for storing values of previous time step ?

};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,16 +77,20 @@ void UnbuiltGaussSeidelConstraintSolver::doSolve(GenericConstraintProblem * prob
{
if(!c_current_cp->constraintsResolutions[i])
{
msg_warning() << "Bad size of constraintsResolutions in GenericConstraintSolver" ;
msg_warning() << "Bad size of constraintsResolutions" ;
c_current_cp->setDimension(i);
break;
}
c_current_cp->constraintsResolutions[i]->init(i, w, force);
i += c_current_cp->constraintsResolutions[i]->getNbLines();
}
memset(force, 0, c_current_cp->getDimension() * sizeof(SReal)); // Erase previous forces for the time being


// zero forces if cold-start
if(!d_initialGuess.getValue())
{
memset(force, 0, c_current_cp->getDimension() * sizeof(SReal));
}

bool showGraphs = false;
sofa::type::vector<SReal>* graph_residuals = nullptr;
std::map < std::string, sofa::type::vector<SReal> > *graph_forces = nullptr, *graph_violations = nullptr;
Expand Down Expand Up @@ -298,4 +302,4 @@ void registerUnbuiltGaussSeidelConstraintSolver(sofa::core::ObjectFactory* facto
.add< UnbuiltGaussSeidelConstraintSolver >());
}

}
}
Loading