# KCSolve Python API Reference The `kcsolve` module provides Python access to the KCSolve pluggable solver framework. It is built with pybind11 and installed alongside the Assembly module. ```python import kcsolve ``` ## Module constants | Name | Value | Description | |------|-------|-------------| | `API_VERSION_MAJOR` | `1` | KCSolve API major version | ## Enums ### BaseJointKind Primitive constraint types. 24 values: `Coincident`, `PointOnLine`, `PointInPlane`, `Concentric`, `Tangent`, `Planar`, `LineInPlane`, `Parallel`, `Perpendicular`, `Angle`, `Fixed`, `Revolute`, `Cylindrical`, `Slider`, `Ball`, `Screw`, `Universal`, `Gear`, `RackPinion`, `Cam`, `Slot`, `DistancePointPoint`, `DistanceCylSph`, `Custom` ### SolveStatus | Value | Meaning | |-------|---------| | `Success` | Solve converged | | `Failed` | Solve did not converge | | `InvalidFlip` | Orientation flipped past threshold | | `NoGroundedParts` | No grounded parts in assembly | ### DiagnosticKind `Redundant`, `Conflicting`, `PartiallyRedundant`, `Malformed` ### MotionKind `Rotational`, `Translational`, `General` ### LimitKind `TranslationMin`, `TranslationMax`, `RotationMin`, `RotationMax` ## Structs ### Transform Rigid-body transform: position + unit quaternion. | Field | Type | Default | Description | |-------|------|---------|-------------| | `position` | `list[float]` (3) | `[0, 0, 0]` | Translation (x, y, z) | | `quaternion` | `list[float]` (4) | `[1, 0, 0, 0]` | Unit quaternion (w, x, y, z) | ```python t = kcsolve.Transform() t = kcsolve.Transform.identity() # same as default ``` Note: quaternion convention is `(w, x, y, z)`, which differs from FreeCAD's `Base.Rotation(x, y, z, w)`. The adapter layer handles conversion. ### Part | Field | Type | Default | |-------|------|---------| | `id` | `str` | `""` | | `placement` | `Transform` | identity | | `mass` | `float` | `1.0` | | `grounded` | `bool` | `False` | ### Constraint A constraint between two parts, built from a FreeCAD JointObject by the adapter layer. | Field | Type | Default | Description | |-------|------|---------|-------------| | `id` | `str` | `""` | FreeCAD document object name (e.g. `"Joint001"`) | | `part_i` | `str` | `""` | Solver-side part ID for first reference | | `marker_i` | `Transform` | identity | Coordinate system on `part_i` (attachment point/orientation) | | `part_j` | `str` | `""` | Solver-side part ID for second reference | | `marker_j` | `Transform` | identity | Coordinate system on `part_j` (attachment point/orientation) | | `type` | `BaseJointKind` | `Coincident` | Constraint type | | `params` | `list[float]` | `[]` | Scalar parameters (interpretation depends on `type`) | | `limits` | `list[Constraint.Limit]` | `[]` | Joint travel limits | | `activated` | `bool` | `True` | Whether this constraint is active | **`marker_i` / `marker_j`** -- Define the local coordinate frames on each part where the joint acts. For example, a Revolute joint's markers define the hinge axis direction and attachment points on each part. **`params`** -- Interpretation depends on `type`: | Type | params[0] | params[1] | |------|-----------|-----------| | `Angle` | angle (radians) | | | `RackPinion` | pitch radius | | | `Screw` | pitch | | | `Gear` | radius I | radius J (negative for belt) | | `DistancePointPoint` | distance | | | `DistanceCylSph` | distance | | | `Planar` | offset | | | `Concentric` | distance | | | `PointInPlane` | offset | | | `LineInPlane` | offset | | ### Constraint.Limit Joint travel limits (translation or rotation bounds). | Field | Type | Default | Description | |-------|------|---------|-------------| | `kind` | `LimitKind` | `TranslationMin` | Which degree of freedom to limit | | `value` | `float` | `0.0` | Limit value (meters for translation, radians for rotation) | | `tolerance` | `float` | `1e-9` | Solver tolerance for limit enforcement | ### MotionDef A motion driver for kinematic simulation. Defines time-dependent actuation of a constraint. | Field | Type | Default | Description | |-------|------|---------|-------------| | `kind` | `MotionKind` | `Rotational` | Type of motion: `Rotational`, `Translational`, or `General` (both) | | `joint_id` | `str` | `""` | ID of the constraint this motion drives | | `marker_i` | `str` | `""` | Reference marker on first part | | `marker_j` | `str` | `""` | Reference marker on second part | | `rotation_expr` | `str` | `""` | Rotation law as a function of time `t` (e.g. `"2*pi*t"`) | | `translation_expr` | `str` | `""` | Translation law as a function of time `t` (e.g. `"10*t"`) | For `Rotational` kind, only `rotation_expr` is used. For `Translational`, only `translation_expr`. For `General`, both are set. ### SimulationParams Time-stepping parameters for kinematic simulation via `run_kinematic()`. | Field | Type | Default | Description | |-------|------|---------|-------------| | `t_start` | `float` | `0.0` | Simulation start time (seconds) | | `t_end` | `float` | `1.0` | Simulation end time (seconds) | | `h_out` | `float` | `0.01` | Output time step -- controls frame rate (e.g. `0.04` = 25 fps) | | `h_min` | `float` | `1e-9` | Minimum internal integration step | | `h_max` | `float` | `1.0` | Maximum internal integration step | | `error_tol` | `float` | `1e-6` | Error tolerance for adaptive time stepping | ### SolveContext Complete input to a solve operation. Built by the adapter layer from FreeCAD document objects, or constructed manually for scripted solving. | Field | Type | Default | Description | |-------|------|---------|-------------| | `parts` | `list[Part]` | `[]` | All parts in the assembly | | `constraints` | `list[Constraint]` | `[]` | Constraints between parts | | `motions` | `list[MotionDef]` | `[]` | Motion drivers for kinematic simulation | | `simulation` | `SimulationParams` or `None` | `None` | Time-stepping parameters for `run_kinematic()` | | `bundle_fixed` | `bool` | `False` | Hint to merge Fixed-joint-connected parts into rigid bodies | **`motions`** -- Motion drivers define time-dependent joint actuation for kinematic simulation. Each `MotionDef` references a constraint by `joint_id` and provides expressions (functions of time `t`) for rotation and/or translation. Only used when calling `run_kinematic()`. **`simulation`** -- When set, provides time-stepping parameters (`t_start`, `t_end`, step sizes, error tolerance) for kinematic simulation via `run_kinematic()`. When `None`, kinematic simulation is not requested. **`bundle_fixed`** -- When `True`, parts connected by `Fixed` joints should be merged into single rigid bodies before solving, reducing the problem size. If the solver reports `supports_bundle_fixed() == True`, it handles this internally. Otherwise, the caller (adapter layer) pre-bundles before building the context. **Important:** pybind11 returns copies of `list` fields, not references. Use whole-list assignment: ```python ctx = kcsolve.SolveContext() p = kcsolve.Part() p.id = "box1" ctx.parts = [p] # correct # ctx.parts.append(p) # does NOT modify ctx ``` ### ConstraintDiagnostic | Field | Type | Default | |-------|------|---------| | `constraint_id` | `str` | `""` | | `kind` | `DiagnosticKind` | `Redundant` | | `detail` | `str` | `""` | ### SolveResult | Field | Type | Default | |-------|------|---------| | `status` | `SolveStatus` | `Success` | | `placements` | `list[SolveResult.PartResult]` | `[]` | | `dof` | `int` | `-1` | | `diagnostics` | `list[ConstraintDiagnostic]` | `[]` | | `num_frames` | `int` | `0` | ### SolveResult.PartResult | Field | Type | Default | |-------|------|---------| | `id` | `str` | `""` | | `placement` | `Transform` | identity | ## Classes ### IKCSolver Abstract base class for solver backends. Subclass in Python to create custom solvers. Three methods must be implemented: ```python class MySolver(kcsolve.IKCSolver): def name(self): return "My Solver" def supported_joints(self): return [kcsolve.BaseJointKind.Fixed, kcsolve.BaseJointKind.Revolute] def solve(self, ctx): result = kcsolve.SolveResult() result.status = kcsolve.SolveStatus.Success return result ``` All other methods are optional and have default implementations. Override them to add capabilities beyond basic solving. #### update(ctx) -> SolveResult Incrementally re-solve after parameter changes (e.g. joint angle adjusted during creation). Solvers can optimize this path since only parameters changed, not topology. Default: delegates to `solve()`. ```python def update(self, ctx): # Only re-evaluate changed constraints, reuse cached factorization return self._incremental_solve(ctx) ``` #### Interactive drag protocol Three-phase protocol for interactive part dragging in the viewport. Solvers can maintain internal state across the drag session for better performance. **pre_drag(ctx, drag_parts) -> SolveResult** -- Prepare for a drag session. `drag_parts` is a `list[str]` of part IDs being dragged. Solve the initial state and cache internal data. Default: delegates to `solve()`. **drag_step(drag_placements) -> SolveResult** -- Called on each mouse move. `drag_placements` is a `list[SolveResult.PartResult]` with the current positions of dragged parts. Returns updated placements for all affected parts. Default: returns Success with no placements. **post_drag()** -- End the drag session and release internal state. Default: no-op. ```python def pre_drag(self, ctx, drag_parts): self._cached_system = self._build_system(ctx) return self.solve(ctx) def drag_step(self, drag_placements): # Use cached system for fast incremental solve for dp in drag_placements: self._cached_system.set_placement(dp.id, dp.placement) return self._cached_system.solve_incremental() def post_drag(self): self._cached_system = None ``` #### Kinematic simulation **run_kinematic(ctx) -> SolveResult** -- Run a kinematic simulation over the time range in `ctx.simulation`. After this call, `num_frames()` returns the frame count and `update_for_frame(i)` retrieves individual frames. Requires `ctx.simulation` to be set and `ctx.motions` to contain at least one motion driver. Default: returns Failed. **num_frames() -> int** -- Number of simulation frames available after `run_kinematic()`. Default: returns 0. **update_for_frame(index) -> SolveResult** -- Retrieve part placements for simulation frame at `index` (0-based, must be < `num_frames()`). Default: returns Failed. ```python # Run a kinematic simulation ctx.simulation = kcsolve.SimulationParams() ctx.simulation.t_start = 0.0 ctx.simulation.t_end = 2.0 ctx.simulation.h_out = 0.04 # 25 fps motion = kcsolve.MotionDef() motion.kind = kcsolve.MotionKind.Rotational motion.joint_id = "Joint001" motion.rotation_expr = "2*pi*t" # one revolution per second ctx.motions = [motion] solver = kcsolve.load("ondsel") result = solver.run_kinematic(ctx) for i in range(solver.num_frames()): frame = solver.update_for_frame(i) for pr in frame.placements: print(f"frame {i}: {pr.id} at {list(pr.placement.position)}") ``` #### diagnose(ctx) -> list[ConstraintDiagnostic] Analyze the assembly for redundant, conflicting, or malformed constraints. May require a prior `solve()` call for some solvers. Returns a list of `ConstraintDiagnostic` objects. Default: returns empty list. ```python diags = solver.diagnose(ctx) for d in diags: if d.kind == kcsolve.DiagnosticKind.Redundant: print(f"Redundant: {d.constraint_id} - {d.detail}") elif d.kind == kcsolve.DiagnosticKind.Conflicting: print(f"Conflict: {d.constraint_id} - {d.detail}") ``` #### is_deterministic() -> bool Whether this solver produces identical results given identical input. Used for regression testing and result caching. Default: returns `True`. #### export_native(path) Write a solver-native debug/diagnostic file (e.g. ASMT format for OndselSolver). Requires a prior `solve()` or `run_kinematic()` call. Default: no-op. ```python solver.solve(ctx) solver.export_native("/tmp/debug.asmt") ``` #### supports_bundle_fixed() -> bool Whether this solver handles Fixed-joint part bundling internally. When `False`, the caller merges Fixed-joint-connected parts into single rigid bodies before building the `SolveContext`, reducing problem size. When `True`, the solver receives unbundled parts and optimizes internally. Default: returns `False`. ### OndselAdapter Built-in solver wrapping OndselSolver's Lagrangian constraint formulation. Inherits `IKCSolver`. ```python solver = kcsolve.OndselAdapter() solver.name() # "OndselSolver (Lagrangian)" ``` In practice, use `kcsolve.load("ondsel")` rather than constructing directly, as this goes through the registry. ## Module functions ### available() Return names of all registered solvers. ```python kcsolve.available() # ["ondsel"] ``` ### load(name="") Create an instance of the named solver. If `name` is empty, uses the default. Returns `None` if the solver is not found. ```python solver = kcsolve.load("ondsel") solver = kcsolve.load() # default solver ``` ### joints_for(name) Query supported joint types for a registered solver. ```python joints = kcsolve.joints_for("ondsel") # [BaseJointKind.Coincident, BaseJointKind.Fixed, ...] ``` ### set_default(name) Set the default solver name. Returns `True` if the name is registered. ```python kcsolve.set_default("ondsel") # True kcsolve.set_default("unknown") # False ``` ### get_default() Get the current default solver name. ```python kcsolve.get_default() # "ondsel" ``` ### register_solver(name, solver_class) Register a Python solver class with the SolverRegistry. `solver_class` must be a callable that returns an `IKCSolver` subclass instance. ```python class MySolver(kcsolve.IKCSolver): def name(self): return "MySolver" def supported_joints(self): return [kcsolve.BaseJointKind.Fixed] def solve(self, ctx): r = kcsolve.SolveResult() r.status = kcsolve.SolveStatus.Success return r kcsolve.register_solver("my_solver", MySolver) solver = kcsolve.load("my_solver") ``` ## Complete example ```python import kcsolve # Build a two-part assembly with a 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] # Solve solver = kcsolve.load("ondsel") result = solver.solve(ctx) print(result.status) # SolveStatus.Success for pr in result.placements: print(f"{pr.id}: pos={list(pr.placement.position)}") # Diagnostics diags = solver.diagnose(ctx) for d in diags: print(f"{d.constraint_id}: {d.kind} - {d.detail}") ``` ## Related - [KCSolve Architecture](../architecture/ondsel-solver.md) - [INTER_SOLVER.md](../../INTER_SOLVER.md) -- full architecture specification