feat(solver): DecompositionSolver entry point and kcsolve registration #30

Open
opened 2026-02-20 22:27:57 +00:00 by forbes · 0 comments
Owner

Summary

Implement solver.py — the IKCSolver subclass that orchestrates the full decomposition pipeline and registers as "decomposition" in the KCSolve solver registry.

Context

This is the thin orchestration layer that ties all components together. It implements the IKCSolver interface (via the pybind11 PySolver trampoline), wiring StructuralAnalyzerDecomposerSubproblemDispatcherReconciler into the solve() method. It also provides the diagnose() fast path that returns structural diagnostics without any numerical solving.

Depends on: #25 (analyzer), #26 (decomposer), #28 (dispatcher), #29 (reconciler)

Design

Class definition

import kcsolve  # or mock interface when running standalone

class DecompositionSolver(kcsolve.IKCSolver):
    """Graph decomposition meta-solver.

    Decomposes assembly constraint problems into subproblems via
    body-bar-hinge graph analysis, dispatches to a numerical backend
    (default: Ondsel), and reconciles sub-solutions.
    """

    def __init__(self, backend_id: str = "ondsel"):
        super().__init__()
        self._backend_id = backend_id
        self._analyzer = StructuralAnalyzer()
        self._decomposer = Decomposer()
        self._dispatcher = SubproblemDispatcher(backend_id)
        self._reconciler = Reconciler()
        self._cached_plan: SolvePlan | None = None

    def name(self) -> str:
        return "Graph Decomposition Solver"

    def supported_joints(self) -> list[BaseJointKind]:
        return kcsolve.joints_for(self._backend_id)

    def solve(self, ctx: SolveContext) -> SolveResult:
        # 1. Structural pre-check
        analysis = self._analyzer.analyze(ctx)
        if analysis.status != SolveStatus.Success:
            return SolveResult(
                status=analysis.status,
                diagnostics=analysis.diagnostics
            )
        # 2. Decompose
        plan = self._decomposer.decompose(ctx)
        self._cached_plan = plan
        # 3. Dispatch
        dispatch_result = self._dispatcher.execute(plan, ctx)
        # 4. Reconcile
        return self._reconciler.merge(ctx, plan, dispatch_result)

    def update(self, ctx: SolveContext) -> SolveResult:
        # Incremental: if cached plan exists, try to re-use decomposition
        # Only re-dispatch affected subproblems
        # Falls back to full solve if structure changed
        if self._cached_plan and self._plan_still_valid(ctx):
            affected = self._find_affected_subproblems(ctx)
            dispatch_result = self._dispatcher.execute_partial(
                self._cached_plan, ctx, affected
            )
            return self._reconciler.merge(ctx, self._cached_plan, dispatch_result)
        return self.solve(ctx)

    def diagnose(self, ctx: SolveContext) -> list[ConstraintDiagnostic]:
        return self._analyzer.diagnose(ctx)

    def is_deterministic(self) -> bool:
        return kcsolve.load(self._backend_id).is_deterministic()

Registration

At module import time or via explicit registration:

def register():
    kcsolve.register_solver("decomposition", DecompositionSolver)

Called from solver/decomposition/__init__.py or from the Create module's addon loader.

Incremental update support

The update() method is the key performance optimization:

  1. Cache the SolvePlan from the last solve() call
  2. On update(), check if the constraint graph structure has changed (new/removed parts or constraints)
  3. If structure unchanged (only parameter changes), identify which subproblems are affected
  4. Re-dispatch only affected subproblems, re-use cached results for unaffected ones
  5. If structure changed, fall back to full solve()

This maps directly to the IKCSolver.update() optional method and enables fast re-solving during joint parameter editing.

Fallback behavior

If the decomposition produces a single subproblem (triconnected graph, no decomposition benefit), the solver should detect this via SolvePlan.is_trivial() and dispatch directly to the backend without decomposition overhead.

Tasks

  • Implement DecompositionSolver class inheriting from kcsolve.IKCSolver
  • Wire all four pipeline stages in solve()
  • Implement diagnose() fast path (structural analysis only)
  • Implement update() with cached plan and incremental re-dispatch
  • Implement supported_joints() delegation to backend
  • Implement is_deterministic() delegation to backend
  • Registration function and __init__.py entry point
  • Trivial plan detection and fallback to direct backend dispatch
  • Unit tests:
    • Full pipeline: SolveContext → decompose → dispatch → reconcile → SolveResult
    • Diagnose-only path returns diagnostics without solving
    • Update with cached plan re-uses unaffected subproblem results
    • Trivial plan falls back to direct dispatch
    • Registration with mock kcsolve registry

Acceptance criteria

  • DecompositionSolver implements the full IKCSolver interface
  • solve() produces correct results for well-constrained assemblies
  • diagnose() returns structural diagnostics without invoking any numerical solver
  • update() demonstrably faster than solve() for parameter-only changes
  • Solver registers as "decomposition" and is discoverable via kcsolve.available()
## Summary Implement `solver.py` — the `IKCSolver` subclass that orchestrates the full decomposition pipeline and registers as `"decomposition"` in the KCSolve solver registry. ## Context This is the thin orchestration layer that ties all components together. It implements the `IKCSolver` interface (via the pybind11 `PySolver` trampoline), wiring `StructuralAnalyzer` → `Decomposer` → `SubproblemDispatcher` → `Reconciler` into the `solve()` method. It also provides the `diagnose()` fast path that returns structural diagnostics without any numerical solving. Depends on: #25 (analyzer), #26 (decomposer), #28 (dispatcher), #29 (reconciler) ## Design ### Class definition ```python import kcsolve # or mock interface when running standalone class DecompositionSolver(kcsolve.IKCSolver): """Graph decomposition meta-solver. Decomposes assembly constraint problems into subproblems via body-bar-hinge graph analysis, dispatches to a numerical backend (default: Ondsel), and reconciles sub-solutions. """ def __init__(self, backend_id: str = "ondsel"): super().__init__() self._backend_id = backend_id self._analyzer = StructuralAnalyzer() self._decomposer = Decomposer() self._dispatcher = SubproblemDispatcher(backend_id) self._reconciler = Reconciler() self._cached_plan: SolvePlan | None = None def name(self) -> str: return "Graph Decomposition Solver" def supported_joints(self) -> list[BaseJointKind]: return kcsolve.joints_for(self._backend_id) def solve(self, ctx: SolveContext) -> SolveResult: # 1. Structural pre-check analysis = self._analyzer.analyze(ctx) if analysis.status != SolveStatus.Success: return SolveResult( status=analysis.status, diagnostics=analysis.diagnostics ) # 2. Decompose plan = self._decomposer.decompose(ctx) self._cached_plan = plan # 3. Dispatch dispatch_result = self._dispatcher.execute(plan, ctx) # 4. Reconcile return self._reconciler.merge(ctx, plan, dispatch_result) def update(self, ctx: SolveContext) -> SolveResult: # Incremental: if cached plan exists, try to re-use decomposition # Only re-dispatch affected subproblems # Falls back to full solve if structure changed if self._cached_plan and self._plan_still_valid(ctx): affected = self._find_affected_subproblems(ctx) dispatch_result = self._dispatcher.execute_partial( self._cached_plan, ctx, affected ) return self._reconciler.merge(ctx, self._cached_plan, dispatch_result) return self.solve(ctx) def diagnose(self, ctx: SolveContext) -> list[ConstraintDiagnostic]: return self._analyzer.diagnose(ctx) def is_deterministic(self) -> bool: return kcsolve.load(self._backend_id).is_deterministic() ``` ### Registration At module import time or via explicit registration: ```python def register(): kcsolve.register_solver("decomposition", DecompositionSolver) ``` Called from `solver/decomposition/__init__.py` or from the Create module's addon loader. ### Incremental update support The `update()` method is the key performance optimization: 1. Cache the `SolvePlan` from the last `solve()` call 2. On `update()`, check if the constraint graph structure has changed (new/removed parts or constraints) 3. If structure unchanged (only parameter changes), identify which subproblems are affected 4. Re-dispatch only affected subproblems, re-use cached results for unaffected ones 5. If structure changed, fall back to full `solve()` This maps directly to the `IKCSolver.update()` optional method and enables fast re-solving during joint parameter editing. ### Fallback behavior If the decomposition produces a single subproblem (triconnected graph, no decomposition benefit), the solver should detect this via `SolvePlan.is_trivial()` and dispatch directly to the backend without decomposition overhead. ## Tasks - [ ] Implement `DecompositionSolver` class inheriting from `kcsolve.IKCSolver` - [ ] Wire all four pipeline stages in `solve()` - [ ] Implement `diagnose()` fast path (structural analysis only) - [ ] Implement `update()` with cached plan and incremental re-dispatch - [ ] Implement `supported_joints()` delegation to backend - [ ] Implement `is_deterministic()` delegation to backend - [ ] Registration function and `__init__.py` entry point - [ ] Trivial plan detection and fallback to direct backend dispatch - [ ] Unit tests: - Full pipeline: SolveContext → decompose → dispatch → reconcile → SolveResult - Diagnose-only path returns diagnostics without solving - Update with cached plan re-uses unaffected subproblem results - Trivial plan falls back to direct dispatch - Registration with mock kcsolve registry ## Acceptance criteria - `DecompositionSolver` implements the full `IKCSolver` interface - `solve()` produces correct results for well-constrained assemblies - `diagnose()` returns structural diagnostics without invoking any numerical solver - `update()` demonstrably faster than `solve()` for parameter-only changes - Solver registers as `"decomposition"` and is discoverable via `kcsolve.available()`
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: kindred/solver#30