JKE
Joint Kinetic Engine — a hardware-language-agnostic validation layer for assemblies. Ships with adapters for build123d, OpenSCAD and plain STL/OBJ meshes; write your own for anything else.
Part files label their mating features (threads, bond pads, bores, dowel holes). An assembly-jke.py file says how those features go together. JKE solves the placements, then runs 34 rules covering thread compatibility, engagement, torque, adhesive bond lines, fits, galvanic pairs, interference, tool access and assembly order — and tells you, with a stable JKE-… code, what will not build.
$ jke validate examples/bad-assembly-jke.py
base~screw_l
x JKE-S008 cover.hole_l is 4 mm across but the M5x0.8-6g fastener is 5 mm; it will not pass through
-> ISO 273 normal fit is 5.5 mm
x JKE-T003 M5x0.5-6H does not accept M5x0.8-6g: pitch differs: 0.5 mm vs 0.8 mm
x JKE-T013 the fastener reaches 15 mm past the stack but the blind hole is only threaded 4 mm deep;
it bottoms out 11 mm before the head seats, so the joint never sees preload
x JKE-A001 'shroud' sits in the 8.5 mm x 28 mm column the folded hex key needs above screw_m5x20.thread
x JKE-M001 MAGNESIUM-AZ31 against STEEL-ZINC-PLATED is a 0.50 V galvanic couple ...
Install
pip install -e ".[dev]" # build123d + pytest
pip install -e . # engine only; geometric rules report themselves as skipped
Part files
A part file stays an ordinary script in whatever language you model in. With build123d: Add one import and declare the features that mate:
from build123d import *
import jke
with BuildPart() as bracket:
Box(40, 20, 10)
with Locations((0, 0, 5)):
Hole(radius=2.1, depth=8) # M5 tap drill
p = jke.part("bracket", bracket, material="AL-6061-T6")
p.thread("mount_a", "M5x0.8-6H", face=p.bore(diameter=4.2)) # depth, axis, blind-ness measured
p.bond_face("pad", face=p.face(normal=(0, 0, -1)), adhesive="3M-DP420", gap=0.2)
Or mark the face inline and let jke.part scan for it:
jke.mark(bracket.faces().sort_by(Axis.Z)[-1], "bond", "lid_pad", adhesive="DP420", gap=0.2)
Or from OpenSCAD (rendered to a mesh behind the scenes), or a bare STL:
from jke.backends.openscad import Model
cover = jke.part("cover", Model("cover.scad", params={"thickness": 6}), material="ABS")
cover.seat("bottom", face=cover.face(normal=(0, 0, -1))) # planar facets are found on meshes
cover.clearance("hole_a", for_thread="M4", thickness=6,
at=jke.Frame.from_axis((-30, -20, 0), (0, 0, -1))) # cylinders are declared
plate = jke.part("plate", "plate.stl", material="steel")
Every declaration accepts either a face= to measure from or explicit numbers (at=, depth=, diameter=…). Where both are given, the declaration wins and JKE-D00x flags any disagreement — so a model can be checked before the geometry is finished, or from a language whose backend cannot measure at all.
| declaration | what it labels |
|---|---|
thread(name, spec, …) |
tapped hole or external thread — "M5x0.8-6H", "1/4-20 UNC-2B", "G1/4", "1/4-18 NPT" |
clearance(name, for_thread="M5", thickness=…) |
a through hole a fastener passes through (sized from ISO 273) |
bond_face(name, …) / seat(name, …) |
an adhesive pad / a face that seats on another |
bore_fit / shaft_fit / dowel_hole / dowel_pin |
cylinders entering a fit — iso_fit="H7" fills in ISO 286 deviations |
Assembly files
import jke
from parts import plate, cover, hardware
screw = hardware.socket_screw("m4x12", "M4x0.7-6g", length=12)
asm = jke.Assembly("sensor_head", environment=jke.Environment("harsh", temp_max=85))
asm.add(plate.plate, "plate") # first part in is the datum
asm.add(cover.lid, "cover")
asm.add(screw, "screw_a")
asm.bolt("screw_a.thread", into="plate.mount_a", through=["cover.hole_a"],
engagement=9.0, torque=2.2, threadlocker="LOCTITE-243")
asm.bond("cover.pad", "pcb.underside", adhesive="SIL-RTV-732", gap=0.6)
asm.press_fit("plate.seat", "bushing.outer")
asm.dowel("plate.dowel_l", "pin_l.lower")
asm.validate().raise_for_errors()
The solver
Nothing has to be pinned. Every unpinned part is a rigid body with six degrees of freedom; every join contributes geometric constraints (coaxial for threads, fits, dowels and each clearance hole in a fastener stack; planar for seats; footprint-coincident for bonds and welds). A breadth-first propagation gives a starting pose, then a Levenberg–Marquardt fit satisfies all the constraints at once. What comes out:
- Over-constrained — constraints that will not go to zero are reported with their residual (
JKE-G001–G004,G009). Two plates whose hole patterns don't match produce exactly this: one bolt lets them slide into line, two or more can't all be satisfied, and the report names each hole and by how much it misses (net of the clearance the hole forgives). - Under-constrained — the Jacobian's null space lists motions nothing prevents: a plate on a flat face with one bolt "can rotate about z" (
JKE-G011). A screw spinning about its own axis is filtered out. - Pinned parts —
asm.add(..., at=(x, y, z))removes a body from the variables; joins touching it become checks on the declared placement.
$ jke validate examples/pattern-mismatch-jke.py # 1/4" vs 1/8" corner insets
one_corner 0 errors (the plates simply slide 1/8")
two_corners x JKE-G009 upper.ne is 4.365 mm off the axis of lower.ne (a 6.6 mm hole
forgives 0.125 mm); the hole patterns on 'upper' and 'lower' do not match
Pure Python; no numpy.
Declarative files and generation
The same assembly as a .jke file — no Python, and parameters overridable from the CLI:
assembly sensor_head
param screw_len = 12
use parts.hardware as hw
part plate = parts.base_plate:plate
part screw_a = hw:socket_screw("m4", "M4x0.7-6g", length={screw_len})
ground plate
bolt screw_a.thread -> plate.mount_a through cover.hole_a engagement=9 torque=2.2
bond cover.sensor_pad ~ pcb.underside adhesive=SIL-RTV-732 gap=0.6
jke validate sensor-assembly.jke screw_len=16
jke depict sensor-assembly.jke --bom
jke generate sensor-assembly.jke -o head.step -o head.stl -o head.png --depict head.md
generate exports the solved assembly — every instance where the joins put it — as STL/OBJ (any mix of backends), STEP/BREP/glTF (all-build123d), or a PNG rendered by a built-in software rasteriser. depict prints the connectivity tree, Mermaid, DOT or a BOM:
lego_tower (16 instances, 88 joins)
[base_a:lego_plate_2x8] ABS-LEGO
^ [c1_a:lego_brick_2x4] ABS-LEGO snap x8 (base_a.s00 into c1_a.a00, ...)
^ [t_a:lego_brick_2x4] ABS-LEGO snap x4 (c1_a.s10 into t_a.a00, ...)
^ [c1_c:lego_brick_2x4] ABS-LEGO snap x4 (c1_c.s10 into t_a.a20, ...)
jke instructions turns the same join graph into a build manual — a parts list, numbered steps with the new parts lifted along their insertion axis and arrows to their seats, what's already built ghosted, multiples marked "4x", and a final view:
A real mechanism works the same way — examples/gearbox/gearbox-assembly.jke is a 16-instance idler-shaft stage (pressed bushing, slip-fit shaft, press-fit pulley with a radial set screw, dowelled and bolted lid, bolted bracket, bonded sight window) that builds in ten steps:
Every arrow is labelled with how it attaches (M5, PRESS, DOWEL, BOND, amber ALIGN for a clearance hole lining up), and each panel's caption spells it out: thread and head style, engagement, torque, fit classes, adhesive and bond line. Rendering is pluggable — register a jke.render.Renderer (or a jke.renderers entry point) and your OCP viewer, Blender or ray tracer receives the placed native shapes, ghost flags, arrows and captions; see docs/adapters.md.
The order is derived from who supports whom (a screw after its target and the plates it passes through, a socket after its stud, a bore-holder after the pin it drops over), lowest course first, so the same command works for a bolted sensor head (examples/sensor_instructions.png).
That tower is examples/lego/tower.jke: jke.contrib.lego builds bricks as real solids with stud/socket ports, and put NAME x y level snaps each one to whatever studs are under it — connectivity is discovered from geometry, then the solver holds every brick by its joins alone. A brick one plate too high is reported disconnected; a brick overlapping another is reported as studs claimed twice plus 1350 mm³ of interference. See docs/jke-files.md.
Backends
JKE never touches geometry directly; it asks a backend a small set of questions (bounding box, is-this-point-inside, which faces are planes/cylinders, how much do these two solids overlap) and every rule is written against the answers. Each backend declares the capability tiers it supports and the rules degrade accordingly — a backend with containment but no booleans gets Monte-Carlo interference checks, a backend with nothing gets only the numeric rules.
| backend | object | capabilities |
|---|---|---|
build123d |
BuildPart, Part, Solid, … |
everything: exact booleans, cylinder/plane extraction, labels |
openscad |
jke.backends.openscad.Model("x.scad", params=…) or a .scad path |
renders via the openscad binary → mesh |
mesh |
.stl / .obj path, or a Mesh |
mass, containment, planar facets, transforms (no dependencies) |
Backends are picked per object, so one assembly can mix languages. Adapters for other languages register with jke.backends.register(...) or a jke.backends entry point, and jke conform reference.stl runs the conformance suite against them. See docs/adapters.md.
Running
jke validate assembly-jke.py # exit 1 if any error
jke validate assembly-jke.py --strict --format json -o report.json
jke validate assembly-jke.py --select JKE-T --ignore JKE-S002
jke validate assembly-jke.py --severity JKE-M001=info --environment controlled
jke inspect parts/base_plate.py # list ports with frames
jke thread "1/4-20 UNC-2B" # dump the numbers behind a designation
jke rules # the catalogue
jke generate x.jke -o x.step -o x.png # export the solved assembly / render it
jke instructions x.jke -o build.png # LEGO-style step-by-step sheet (--pages DIR for one per step)
jke depict x.jke --format mermaid # connectivity as a graph
jke conform reference.scad # check a backend against the reference solid
jke doctor # installed backends and their capabilities
python assembly-jke.py works too — asm.validate() returns a Report with .errors, .warnings, .to_text(), .to_markdown(), .to_json().
What gets checked
See docs/rules.md for the full table. In outline:
| family | covers |
|---|---|
| T threads | standard / diameter / pitch / hand / starts / taper match, tolerance-class genders and allowance, engagement vs material (1×D steel … 3×D printed plastic), bottoming in blind holes, protrusion through tapped holes, stripping length, torque vs proof load and vs thread shear, threadlocker suitability, tapping plastics, stainless galling, sealing on parallel threads |
| S structure | fastener passes through its clearance stack, head bearing / counterbore, grip length vs shank, ports used once, unused labels, disconnected instances, unplaceable parts, wrong port kinds |
| B bonding | adhesive named, bond-line inside the qualified window (incl. fixed-thickness tapes), substrate qualification and surface energy, service temperature, shear stress vs allowable, peel/cleavage loading, CTE-mismatch strain |
| N snaps | a snap/clutch feature has interference, but not enough to yield |
| F fits | a 10 mm shaft does not go in a 4 mm bore (and a 4 mm shaft rattles in a 10 mm one), ISO 286 classes vs intent (press / slip / transition), Lamé hoop stress in the hub, loss of interference at tolerance extremes or temperature, press force estimate, polymer relaxation, over-doweling |
| G geometry | axial / radial / angular residuals on every join and every clearance hole in a stack (hole-pattern mismatch), under-constrained motions, faces that don't oppose, port normals pointing into their own part, solid interference net of what each join legitimately explains |
| A access | driver envelope and swing room above each head, counterbore admits the tool, insertion corridor, a topological assembly order exists, permanent joints in a serviceable assembly |
| M materials | galvanic couples vs environment, service-temperature limits, thermal preload change, polymers under clamp load |
| D declaration | tap-drill sanity (modelled vs minor diameter), declared vs measured lengths, partial cylindrical faces, through-hole mouth ambiguity, missing solids / materials |
Rules never raise: anything that can't be evaluated (no kernel, missing material) becomes a skipped finding. Add your own with @jke.rules.register on a JoinRule or AssemblyRule subclass.
Layout
jke/
part.py, assembly.py authoring API
ports.py, joins.py the model the rules see
solver.py, constraints.py, rigid-body constraint solver (LM + null-space analysis);
numeric.py, _math.py dependency-free linear algebra and 3D math
geometry.py backend-neutral geometry queries (+ sampled booleans)
backends/ the adapter contract, registry, conformance suite, and the
build123d / openscad / mesh adapters
threads.py, fits.py, ISO 68/965/261, ASME B1.1, ISO 286, ISO 273,
fasteners.py, materials.py, MIL-STD-889 anodic indices, adhesive windows
adhesives.py
rules/ one file per family; each rule has a stable code
generate.py, raster.py solved-assembly export, depiction, BOM; PNG software renderer
render.py the SceneSpec / Renderer contract for external renderers
instructions.py step-by-step build sheets (order from the join graph)
jkefile.py the .jke declarative format
contrib/lego.py LEGO-compatible elements and grid placement
report.py, config.py, cli.py
examples/ a clean assembly (.py and .jke), a deliberately broken one,
the mismatched-hole-pattern plates, a build123d + OpenSCAD
mixed assembly, a 16-piece LEGO tower, and a 16-part gearbox stage
tests/ 148 tests; CAD-dependent ones skip without build123d
Units are millimetres, degrees, newtons, MPa. Strings like "0.25in" or "1/4 in" are accepted wherever a length is.
Metadata
Release files for jke 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| jke-0.1.0.tar.gz | 176.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| jke-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 350.6 kB
Release files / jke-0.1.0.tar.gz
| Download URL | jke-0.1.0.tar.gz |
|---|---|
| Size | 176.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9ad7447ae68077bf068652f03489cd7daf6d0154e087f89976eb95653607c65d
|
|
BLAKE2b-256 checksum How to use checksums |
cdff5ab6b874f7e8056537af8ae1d3404f227edd83e0bedde82ab67923dedcf0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 21, 2026.
Transparency logRelease files / jke-0.1.0-py3-none-any.whl
| Download URL | jke-0.1.0-py3-none-any.whl |
|---|---|
| Size | 174.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2f84a7c60dd26727f4e3c15bb4e2a54300417b017a03af64b6c0a37c6d2e9b26
|
|
BLAKE2b-256 checksum How to use checksums |
6d820ea5fe884470ec84ef197f933fb1b942242f9c591a9c4b7ad5d80c2e8ec8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 21, 2026.
Transparency log