Metadata-Version: 2.4
Name: jointke
Version: 0.0.1
Summary: JointKE - a kinetic/joint validation engine for build123d assemblies
Author: Steve Zeng
License: Apache-2.0
Keywords: cad,build123d,assembly,joints,threads,validation,dfa
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Manufacturing
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: cad
Requires-Dist: build123d>=0.9; extra == "cad"
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: build123d>=0.9; extra == "dev"
Dynamic: license-file

# JointKE

**Joint Kinetic Engine** — a validation layer for [build123d](https://github.com/gumyr/build123d) assemblies.

Part files label their mating features (threads, bond pads, bores, dowel holes). An `assembly-jke.py` file says how those features go together. JointKE 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

```sh
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 build123d script. Add one import and declare the features that mate:

```python
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:

```python
jke.mark(bracket.faces().sort_by(Axis.Z)[-1], "bond", "lid_pad", adhesive="DP420", gap=0.2)
```

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.

| 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

```python
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.

## Running

```sh
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 doctor
```

`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](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 |
| **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                 the only module that imports build123d
  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
  report.py, config.py, cli.py
examples/                     a clean assembly, a deliberately broken one, and the
                                mismatched-hole-pattern plates
tests/                        115 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.
