feat(assembly): add diagnostic logging to solver and assembly
Some checks failed
Build and Test / build (pull_request) Has been cancelled

C++ (AssemblyObject):
- getOrCreateSolver: log which solver backend was loaded
- solve: log assembly name, grounded/joint counts, context size,
  result status with DOF and placement count, per-constraint
  diagnostics on failure
- preDrag/doDragStep/postDrag: log drag part count, per-step
  validation failures, and summary (total steps / rejected count)
- buildSolveContext: log grounded/free part counts, constraint count,
  limits count, and bundle_fixed flag

Python (kindred_solver submodule):
- solver.py: log solve entry/exit with timing, system build stats,
  decomposition decisions, Newton/BFGS fallback events, drag lifecycle
- decompose.py: log cluster stats and per-cluster convergence
- Init.py: FreeCAD log handler routing Python logging to Console
This commit is contained in:
forbes
2026-02-21 10:08:10 -06:00
parent 0f8fa0be86
commit c682c5d153
3 changed files with 64 additions and 2 deletions

View File

@@ -176,6 +176,10 @@ KCSolve::IKCSolver* AssemblyObject::getOrCreateSolver()
std::string solverName = hGrp->GetASCII("Solver", "");
solver_ = KCSolve::SolverRegistry::instance().get(solverName);
// get("") returns the registry default (first registered solver)
if (solver_) {
FC_LOG("Assembly : loaded solver '" << solver_->name()
<< "' (requested='" << solverName << "')");
}
}
return solver_.get();
}
@@ -212,14 +216,22 @@ int AssemblyObject::solve(bool enableRedo, bool updateJCS)
auto groundedObjs = getGroundedParts();
if (groundedObjs.empty()) {
FC_LOG("Assembly : solve skipped — no grounded parts");
return -6;
}
std::vector<App::DocumentObject*> joints = getJoints(updateJCS);
removeUnconnectedJoints(joints, groundedObjs);
FC_LOG("Assembly : solve on '" << getFullLabel()
<< "' — " << groundedObjs.size() << " grounded, "
<< joints.size() << " joints");
KCSolve::SolveContext ctx = buildSolveContext(joints);
FC_LOG("Assembly : solve context — " << ctx.parts.size() << " parts, "
<< ctx.constraints.size() << " constraints");
// Always save placements to enable orientation flip detection
savePlacementsForUndo();
@@ -241,6 +253,13 @@ int AssemblyObject::solve(bool enableRedo, bool updateJCS)
}
if (lastResult_.status == KCSolve::SolveStatus::Failed) {
FC_LOG("Assembly : solve failed — status="
<< static_cast<int>(lastResult_.status)
<< ", " << lastResult_.diagnostics.size() << " diagnostics");
for (const auto& d : lastResult_.diagnostics) {
Base::Console().warning("Assembly : diagnostic [%s]: %s\n",
d.constraint_id.c_str(), d.detail.c_str());
}
updateSolveStatus();
return -1;
}
@@ -248,6 +267,7 @@ int AssemblyObject::solve(bool enableRedo, bool updateJCS)
// Validate that the solve didn't cause any parts to flip orientation
if (!validateNewPlacements()) {
// Restore previous placements - the solve found an invalid configuration
FC_LOG("Assembly : solve rejected — placement validation failed, undoing");
undoSolve();
lastSolverStatus = -2;
updateSolveStatus();
@@ -265,6 +285,9 @@ int AssemblyObject::solve(bool enableRedo, bool updateJCS)
updateSolveStatus();
FC_LOG("Assembly : solve succeeded — dof=" << lastResult_.dof
<< ", " << lastResult_.placements.size() << " placements");
return 0;
}
@@ -409,6 +432,8 @@ size_t Assembly::AssemblyObject::numberOfFrames()
void AssemblyObject::preDrag(std::vector<App::DocumentObject*> dragParts)
{
bundleFixed = true;
dragStepCount_ = 0;
dragStepRejected_ = 0;
auto* solver = getOrCreateSolver();
if (!solver) {
@@ -421,6 +446,7 @@ void AssemblyObject::preDrag(std::vector<App::DocumentObject*> dragParts)
auto groundedObjs = getGroundedParts();
if (groundedObjs.empty()) {
FC_LOG("Assembly : preDrag skipped — no grounded parts");
bundleFixed = false;
return;
}
@@ -474,6 +500,10 @@ void AssemblyObject::preDrag(std::vector<App::DocumentObject*> dragParts)
}
}
FC_LOG("Assembly : preDrag — " << dragPartIds.size() << " drag part(s), "
<< joints.size() << " joints, " << ctx.parts.size() << " parts, "
<< ctx.constraints.size() << " constraints");
savePlacementsForUndo();
try {
@@ -482,11 +512,13 @@ void AssemblyObject::preDrag(std::vector<App::DocumentObject*> dragParts)
}
catch (...) {
// If pre_drag fails, we still need to be in a valid state
FC_LOG("Assembly : preDrag — solver pre_drag threw exception");
}
}
void AssemblyObject::doDragStep()
{
dragStepCount_++;
try {
std::vector<KCSolve::SolveResult::PartResult> dragPlacements;
@@ -506,6 +538,10 @@ void AssemblyObject::doDragStep()
lastResult_ = solver_->drag_step(dragPlacements);
if (lastResult_.status == KCSolve::SolveStatus::Failed) {
FC_LOG("Assembly : dragStep #" << dragStepCount_ << " — solver failed");
}
if (validateNewPlacements()) {
setNewPlacements();
@@ -517,9 +553,12 @@ void AssemblyObject::doDragStep()
}
}
}
else {
dragStepRejected_++;
}
}
catch (...) {
// We do nothing if a solve step fails.
FC_LOG("Assembly : dragStep #" << dragStepCount_ << " — exception");
}
}
@@ -631,6 +670,8 @@ bool AssemblyObject::validateNewPlacements()
void AssemblyObject::postDrag()
{
FC_LOG("Assembly : postDrag — " << dragStepCount_ << " steps, "
<< dragStepRejected_ << " rejected");
if (solver_) {
solver_->post_drag();
}
@@ -1342,6 +1383,23 @@ KCSolve::SolveContext AssemblyObject::buildSolveContext(
ctx.simulation = sp;
}
// Log context summary
{
int nGrounded = 0, nFree = 0, nLimits = 0;
for (const auto& p : ctx.parts) {
if (p.grounded) nGrounded++;
else nFree++;
}
for (const auto& c : ctx.constraints) {
if (!c.limits.empty()) nLimits++;
}
FC_LOG("Assembly : buildSolveContext — "
<< nGrounded << " grounded + " << nFree << " free parts, "
<< ctx.constraints.size() << " constraints"
<< (nLimits ? (std::string(", ") + std::to_string(nLimits) + " with limits") : "")
<< (ctx.bundle_fixed ? ", bundle_fixed=true" : ""));
}
return ctx;
}