Files
create/docs/src/reference/kcsolve-python.md
forbes bd43e62822
All checks were successful
Build and Test / build (pull_request) Successful in 29m49s
docs(kcsolve): expand Python API reference with full method docs
Expand SolveContext field descriptions (motions, simulation, bundle_fixed),
Constraint params table, marker explanations, Constraint.Limit descriptions,
MotionDef field descriptions, SimulationParams field descriptions, and all
optional IKCSolver methods with signatures, parameter docs, and usage
examples (interactive drag protocol, kinematic simulation, diagnostics,
export_native, capability queries).
2026-02-19 19:06:08 -06:00

15 KiB

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.

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)
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:

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:

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().

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.

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.

# 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.

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.

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.

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.

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.

solver = kcsolve.load("ondsel")
solver = kcsolve.load()  # default solver

joints_for(name)

Query supported joint types for a registered solver.

joints = kcsolve.joints_for("ondsel")
# [BaseJointKind.Coincident, BaseJointKind.Fixed, ...]

set_default(name)

Set the default solver name. Returns True if the name is registered.

kcsolve.set_default("ondsel")  # True
kcsolve.set_default("unknown")  # False

get_default()

Get the current default solver name.

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.

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

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}")