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).
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}")
Related
- KCSolve Architecture
- INTER_SOLVER.md -- full architecture specification