# Assembly Integration The Kindred solver integrates with FreeCAD's Assembly workbench through the KCSolve pluggable solver framework. This page describes the bridge layer, preference system, and interactive drag protocol. ## KindredSolver class **Source:** `mods/solver/kindred_solver/solver.py` `KindredSolver` subclasses `kcsolve.IKCSolver` and implements the solver interface: ```python class KindredSolver(kcsolve.IKCSolver): def name(self): return "Kindred (Newton-Raphson)" def supported_joints(self): return list(_SUPPORTED) # 20 of 24 BaseJointKind values def solve(self, ctx): # Static solve def diagnose(self, ctx): # Constraint analysis def pre_drag(self, ctx, drag_parts): # Begin drag session def drag_step(self, drag_placements): # Mouse move during drag def post_drag(self): # End drag session def is_deterministic(self): # Returns True ``` ### Registration The solver is registered at addon load time via `Init.py`: ```python import kcsolve from kindred_solver import KindredSolver kcsolve.register_solver("kindred", KindredSolver) ``` The `mods/solver/` directory is a FreeCAD addon discovered by the addon loader through its `package.xml` manifest. ### Supported joints The Kindred solver handles 20 of the 24 `BaseJointKind` values. The remaining 4 are stubs that produce no residuals: | Supported | Stub (no residuals) | |-----------|-------------------| | Coincident, PointOnLine, PointInPlane, Concentric, Tangent, Planar, LineInPlane, Parallel, Perpendicular, Angle, Fixed, Revolute, Cylindrical, Slider, Ball, Screw, Universal, Gear, RackPinion, DistancePointPoint | Cam, Slot, DistanceCylSph, Custom | ### Joint limits Joint travel limits (`Constraint.limits`) are accepted but not enforced. The solver logs a warning once per instance when limits are encountered. Enforcing inequality constraints requires active-set or barrier-method extensions beyond the current Newton-Raphson formulation. ## Solver selection ### C++ preference `AssemblyObject::getOrCreateSolver()` reads the user preference to select the solver backend: ```cpp ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath( "User parameter:BaseApp/Preferences/Mod/Assembly"); std::string solverName = hGrp->GetASCII("Solver", ""); solver_ = KCSolve::SolverRegistry::instance().get(solverName); ``` An empty string (`""`) returns the registry default (the first solver registered, which is OndselSolver). Setting `"kindred"` selects the Kindred solver. `resetSolver()` clears the cached solver instance so the next solve picks up preference changes. ### Preferences UI The Assembly preferences page (`Edit > Preferences > Assembly`) includes a "Solver backend" combo box populated from the registry at load time: - **Default** -- empty string, uses the registry default (OndselSolver) - **OndselSolver (Lagrangian)** -- `"ondsel"` - **Kindred (Newton-Raphson)** -- `"kindred"` (available when the solver addon is loaded) The preference is stored as `Mod/Assembly/Solver` in the FreeCAD parameter system. ### Programmatic switching From the Python console: ```python import FreeCAD pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Assembly") # Switch to Kindred pref.SetString("Solver", "kindred") # Switch back to default pref.SetString("Solver", "") # Force the active assembly to pick up the change if hasattr(FreeCADGui, "ActiveDocument"): for obj in FreeCAD.ActiveDocument.Objects: if hasattr(obj, "resetSolver"): obj.resetSolver() ``` ## Interactive drag protocol The drag protocol provides real-time constraint solving during viewport part dragging. It is a three-phase protocol with a caching layer that avoids rebuilding the constraint system on every mouse move. ### pre_drag(ctx, drag_parts) Called when the user begins dragging. Builds the constraint system once, runs the substitution pre-pass, constructs the symbolic Jacobian, compiles the evaluator, performs an initial solve, and caches everything in a `_DragCache` for reuse across subsequent `drag_step()` calls. ```python def pre_drag(self, ctx, drag_parts): self._drag_ctx = ctx self._drag_parts = set(drag_parts) system = _build_system(ctx) half_spaces = compute_half_spaces(...) weight_vec = build_weight_vector(system.params) residuals = substitution_pass(system.all_residuals, system.params) # single_equation_pass is intentionally skipped — it bakes variable # values as constants that become stale when dragged parts move. jac_exprs = [[r.diff(name).simplify() for name in free] for r in residuals] compiled_eval = try_compile_system(residuals, jac_exprs, ...) # Initial solve (Newton-Raphson + BFGS fallback) newton_solve(residuals, system.params, ...) # Cache for drag_step() reuse cache = _DragCache() cache.system = system cache.residuals = residuals cache.jac_exprs = jac_exprs cache.compiled_eval = compiled_eval cache.half_spaces = half_spaces cache.weight_vec = weight_vec ... return result ``` **Important:** `single_equation_pass` is not used in the drag path. It analytically solves single-variable equations and bakes the results as `Const()` nodes into downstream expressions. During drag, those baked values become stale when part positions change, causing constraints to silently stop being enforced. Only `substitution_pass` (which replaces genuinely grounded parameters) is safe to cache. ### drag_step(drag_placements) Called on each mouse move. Updates only the dragged part's 7 parameter values in the cached `ParamTable`, then re-solves using the cached residuals, Jacobian, and compiled evaluator. No system rebuild occurs. ```python def drag_step(self, drag_placements): cache = self._drag_cache params = cache.system.params # Update only the dragged part's parameters for pr in drag_placements: pfx = pr.id + "/" params.set_value(pfx + "tx", pr.placement.position[0]) params.set_value(pfx + "ty", pr.placement.position[1]) params.set_value(pfx + "tz", pr.placement.position[2]) params.set_value(pfx + "qw", pr.placement.quaternion[0]) params.set_value(pfx + "qx", pr.placement.quaternion[1]) params.set_value(pfx + "qy", pr.placement.quaternion[2]) params.set_value(pfx + "qz", pr.placement.quaternion[3]) # Solve with cached artifacts — no rebuild newton_solve(cache.residuals, params, ..., jac_exprs=cache.jac_exprs, compiled_eval=cache.compiled_eval) return result ``` ### post_drag() Called when the drag ends. Clears the cached state. ```python def post_drag(self): self._drag_ctx = None self._drag_parts = None self._drag_cache = None ``` ### _DragCache The cache holds all artifacts built in `pre_drag()` that are invariant across drag steps (constraint topology doesn't change during a drag): | Field | Contents | |-------|----------| | `system` | `_System` -- owns `ParamTable` and `Expr` trees | | `residuals` | `list[Expr]` -- after substitution pass | | `jac_exprs` | `list[list[Expr]]` -- symbolic Jacobian | | `compiled_eval` | `Callable` or `None` -- native compiled evaluator | | `half_spaces` | `list[HalfSpace]` -- branch trackers | | `weight_vec` | `ndarray` or `None` -- minimum-movement weights | | `post_step_fn` | `Callable` or `None` -- half-space correction callback | ### Performance The caching layer eliminates the expensive per-frame overhead (~150 ms for system build + Jacobian construction + compilation). Each `drag_step()` only evaluates the cached expressions at updated parameter values: - Newton-Raphson converges in 1-2 iterations from a nearby initial guess - The compiled evaluator (`codegen.py`) uses native Python `exec` for flat evaluation, avoiding the recursive tree-walk overhead - The substitution pass compiles grounded-body parameters to constants, reducing the effective system size - DOF counting is skipped during drag for speed (`result.dof = -1`) ## Diagnostics integration `diagnose(ctx)` builds the constraint system and runs overconstrained detection, returning a list of `kcsolve.ConstraintDiagnostic` objects. The Assembly module calls this to populate the constraint diagnostics panel. ```python def diagnose(self, ctx): system = _build_system(ctx) residuals = substitution_pass(system.all_residuals, system.params) return _run_diagnostics(residuals, system.params, system.residual_ranges, ctx) ``` ## Not yet implemented - **Kinematic simulation** (`run_kinematic`, `num_frames`, `update_for_frame`) -- the base class defaults return `Failed`. Requires time-stepping integration with motion driver expression evaluation. - **Joint limit enforcement** -- inequality constraints need active-set or barrier solver extensions. - **Fixed-joint bundling** (`supports_bundle_fixed()` returns `False`) -- the solver receives unbundled parts; the Assembly module pre-bundles when needed. - **Native export** (`export_native()`) -- no solver-native debug format defined.