# Writing a Custom Solver The KCSolve framework lets you implement a solver backend in pure Python, register it at runtime, and select it through the Assembly preferences. This tutorial walks through building a minimal solver and then extending it. ## Minimal solver A solver must subclass `kcsolve.IKCSolver` and implement three methods: ```python import kcsolve class MySolver(kcsolve.IKCSolver): def __init__(self): super().__init__() # required for pybind11 trampoline def name(self): return "My Custom Solver" def supported_joints(self): return [ kcsolve.BaseJointKind.Fixed, kcsolve.BaseJointKind.Revolute, ] def solve(self, ctx): result = kcsolve.SolveResult() # Find grounded parts grounded = {p.id for p in ctx.parts if p.grounded} if not grounded: result.status = kcsolve.SolveStatus.NoGroundedParts return result # Your solving logic here... # For each non-grounded part, compute its solved placement for part in ctx.parts: if part.grounded: continue pr = kcsolve.SolveResult.PartResult() pr.id = part.id pr.placement = part.placement # use current placement as placeholder result.placements = result.placements + [pr] result.status = kcsolve.SolveStatus.Success result.dof = 0 return result ``` Register it: ```python kcsolve.register_solver("my_solver", MySolver) ``` Test it from the FreeCAD console: ```python solver = kcsolve.load("my_solver") print(solver.name()) # "My Custom Solver" ctx = kcsolve.SolveContext() # ... build context ... result = solver.solve(ctx) print(result.status) # SolveStatus.Success ``` ## Addon packaging To make your solver load automatically, create a FreeCAD addon: ``` my_solver_addon/ package.xml # Addon manifest Init.py # Registration entry point my_solver/ __init__.py solver.py # MySolver class ``` **package.xml:** ```xml MyCustomSolver Custom assembly constraint solver 0.1.0 MySolver ``` **Init.py:** ```python import kcsolve from my_solver.solver import MySolver kcsolve.register_solver("my_solver", MySolver) ``` Place the addon in the FreeCAD Mod directory or as a git submodule in `mods/`. ## Working with SolveContext The `SolveContext` contains everything the solver needs: ### Parts ```python for part in ctx.parts: print(f"{part.id}: grounded={part.grounded}") print(f" position: {list(part.placement.position)}") print(f" quaternion: {list(part.placement.quaternion)}") print(f" mass: {part.mass}") ``` Each part has 7 degrees of freedom: 3 translation (x, y, z) and 4 quaternion components (w, x, y, z) with a unit-norm constraint reducing the rotational DOF to 3. **Quaternion convention:** `(w, x, y, z)` where `w` is the scalar part. This differs from FreeCAD's `Base.Rotation(x, y, z, w)`. The adapter layer handles the swap. ### Constraints ```python for c in ctx.constraints: if not c.activated: continue print(f"{c.id}: {c.type} between {c.part_i} and {c.part_j}") print(f" marker_i: pos={list(c.marker_i.position)}, " f"quat={list(c.marker_i.quaternion)}") print(f" params: {list(c.params)}") print(f" limits: {len(c.limits)}") ``` The marker transforms define local coordinate frames on each part. The constraint type determines what geometric relationship is enforced between these frames. ### Returning results ```python result = kcsolve.SolveResult() result.status = kcsolve.SolveStatus.Success result.dof = computed_dof placements = [] for part_id, pos, quat in solved_parts: pr = kcsolve.SolveResult.PartResult() pr.id = part_id pr.placement = kcsolve.Transform() pr.placement.position = list(pos) pr.placement.quaternion = list(quat) placements.append(pr) result.placements = placements return result ``` **Important:** pybind11 list fields return copies. Use `result.placements = [...]` (whole-list assignment), not `result.placements.append(...)`. ## Adding optional capabilities ### Diagnostics Override `diagnose()` to detect overconstrained or malformed assemblies: ```python def diagnose(self, ctx): diagnostics = [] # ... analyze constraints ... d = kcsolve.ConstraintDiagnostic() d.constraint_id = "Joint001" d.kind = kcsolve.DiagnosticKind.Redundant d.detail = "This joint duplicates Joint002" diagnostics.append(d) return diagnostics ``` ### Interactive drag Override the three drag methods for real-time viewport dragging: ```python def pre_drag(self, ctx, drag_parts): self._ctx = ctx self._dragging = set(drag_parts) return self.solve(ctx) def drag_step(self, drag_placements): # Update dragged parts in stored context for pr in drag_placements: for part in self._ctx.parts: if part.id == pr.id: part.placement = pr.placement break return self.solve(self._ctx) def post_drag(self): self._ctx = None self._dragging = None ``` For responsive dragging, the solver should converge quickly from a nearby initial guess. Use warm-starting (current placements as initial guess) and consider caching internal state across drag steps. ### Incremental update Override `update()` for the case where only constraint parameters changed (not topology): ```python def update(self, ctx): # Reuse cached factorization, only re-evaluate changed residuals return self.solve(ctx) # default: just re-solve ``` ## Testing ### Unit tests (without FreeCAD) Test your solver logic with hand-built `SolveContext` objects: ```python import kcsolve def test_fixed_joint(): ctx = kcsolve.SolveContext() base = kcsolve.Part() base.id = "base" base.grounded = True arm = kcsolve.Part() arm.id = "arm" arm.placement.position = [100.0, 0.0, 0.0] joint = kcsolve.Constraint() joint.id = "Joint001" joint.part_i = "base" joint.part_j = "arm" joint.type = kcsolve.BaseJointKind.Fixed ctx.parts = [base, arm] ctx.constraints = [joint] solver = MySolver() result = solver.solve(ctx) assert result.status == kcsolve.SolveStatus.Success ``` ### Integration tests (with FreeCAD) For integration testing within FreeCAD, follow the pattern in `TestKindredSolverIntegration.py`: set the solver preference in `setUp()`, create document objects, and verify solve results. ## Reference - [KCSolve Python API](../reference/kcsolve-python.md) -- complete type and function reference - [KCSolve Architecture](../architecture/ondsel-solver.md) -- C++ framework details - [Constraints](constraints.md) -- constraint types and residual counts - [Kindred Solver Overview](overview.md) -- how the built-in Kindred solver works