Files
create/docs/src/reference/kcsolve-python.md
forbes 406e120180
All checks were successful
Build and Test / build (pull_request) Successful in 28m58s
docs: KCSolve architecture and Python API reference
- Replace OndselSolver architecture doc with KCSolve pluggable solver
  architecture covering IKCSolver interface, SolverRegistry, OndselAdapter,
  Python bindings, file layout, and testing
- Add kcsolve Python API reference with full type documentation, module
  functions, usage examples, and pybind11 vector-copy caveat
- Add INTER_SOLVER.md spec (previously untracked) with Phase 1 and Phase 2
  marked as complete
- Update SUMMARY.md with new page links
2026-02-19 18:59:05 -06:00

7.7 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

Field Type Default
id str ""
part_i str ""
marker_i Transform identity
part_j str ""
marker_j Transform identity
type BaseJointKind Coincident
params list[float] []
limits list[Constraint.Limit] []
activated bool True

Constraint.Limit

Field Type Default
kind LimitKind TranslationMin
value float 0.0
tolerance float 1e-9

MotionDef

Field Type Default
kind MotionKind Rotational
joint_id str ""
marker_i str ""
marker_j str ""
rotation_expr str ""
translation_expr str ""

SimulationParams

Field Type Default
t_start float 0.0
t_end float 1.0
h_out float 0.01
h_min float 1e-9
h_max float 1.0
error_tol float 1e-6

SolveContext

Field Type Default
parts list[Part] []
constraints list[Constraint] []
motions list[MotionDef] []
simulation SimulationParams or None None
bundle_fixed bool False

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

Optional overrides (all have default implementations):

Method Default behavior
update(ctx) Delegates to solve()
pre_drag(ctx, drag_parts) Delegates to solve()
drag_step(drag_placements) Returns Success with no placements
post_drag() No-op
run_kinematic(ctx) Returns Failed
num_frames() Returns 0
update_for_frame(index) Returns Failed
diagnose(ctx) Returns empty list
is_deterministic() Returns True
export_native(path) No-op
supports_bundle_fixed() 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}")