This release is a pre-release and may not be stable for production use.
Standard hardware assets generator for Physical AI.
Quincaillerie (French for hardware, the nuts and bolts kind) generates
metric screws, nuts and washers with modelled threads, and battery
cells with the holders they go in, and exports them to OpenUSD as
physics-ready assets: rigid bodies with real masses and inertias, convex
collision pieces that follow the thread's helix, and holders whose spring or
clip is a UsdPhysics joint with a drive. A nut screws onto its screw, one
pitch per turn; a cell pushed into its holder compresses the spring with the
force it was designed for, and a 9 V battery put in backwards does not fit.
No two parts are the same. A real box of M8 screws is not one screw a hundred times: some threads fit tighter than others, a few are damaged, some are rusty or oily, springs get tired. Quincaillerie draws every instance from those distributions: tolerances, defects, wear and physics, each with its own random stream, and writes what it drew into the file and into a CSV, as ground truth. The variation is not paint: a crushed thread really stops a nut, a tired spring really presses less, a rusty screw really has more friction.
The assets are checked in MuJoCo and in Blender: dropped on a floor, assembled, screwed, inserted, without coming apart. A Blender add-on sets the rigid bodies and constraints up by itself when a file is imported, so that pressing Play just works.
It is built for Physical AI: robots that assemble, sort, screw and swap batteries are trained and tested in simulation, and they need small parts with exact geometry and physics, not a cylinder named "bolt". Everything can run on CPU. Quincaillerie is the fourth project of a series, after Granulaire (granular assets), Maillage (textiles) and Filaire (cables), and follows the same approach: distributions instead of one shape, measured outputs, honest limits.
ISO 4017 screws, ISO 7089 washers and ISO 4032 nuts, M3 to M12, in the six finishes of the material library. The nuts sit on the thread at the height their rotation gives them; the loose parts in front were dropped in MuJoCo and rest where they fell. Imported from the OpenUSD files into Blender and rendered with Cycles.
Every instance is different
quincaillerie generate screw M10 --with nut,washer --count 10 and the same
for AA and 9 V cells: each part is a distinct file. Rust, grime, oil and
scratches; scuffed and faded labels; a dished washer, oval and crushed nuts,
crushed and bent screws (not all visible from here); and, invisible in a
picture, a different thread fit, alloy density, friction and spring for each.
| Aspect | Fasteners | Cells | Holders |
|---|---|---|---|
| tolerance | thread clearance 1.6–8 % of the pitch per side (tight to loose fit; 0.02–0.10 mm at M8), across flats, head and nut height, length | diameter and length under nominal, within the IEC bands | side play of the cradle (0–0.6 mm) |
defect (--defect-rate, 15 %) |
crushed thread (crests pushed sideways over 40–140°: jams a nut), bent shank (0.8–3°), burr at the tip; oval or crushed nut; dished washer | dented can, corroded terminal (crust) | |
| wear | new, used, oily, rusty, dirty: rust, oil, grime and scratches baked into the instance's own textures; friction follows (rust ×1.8 at most, oil ×0.6) | scuffed and faded label, grime | grey or black ABS |
| physics | density of the alloy (brass and stainless are solid alloys, the rest plated steel), friction | mass ±3 %, friction | spring stiffness ±8 %, tired springs (preload down to 70 %), clip torque ±10 %, friction |
Each aspect has its own random stream: --no-vary wear keeps the same
tolerances and defects with clean parts; the same seed gives the same parts.
Everything drawn is in parts.csv and in each file as
quincaillerie:variation:* attributes; the clean finishes stay available in
the finish variant set of a worn part. --nominal gives the reference part.
The defects are physical. The same nut, the same light torque (a hand on a
nut driver), simulated in MuJoCo from the files: it runs down the sound
screw (left) and stops at the crushed thread of the other (right).
quincaillerie sim-test --defects checks this for every crushed screw of a
batch.
The physics, in motion
| Dropped in Blender | Screwed in MuJoCo | Inserted in MuJoCo |
|---|---|---|
| Two screw + washer + nut assemblies, an AA cell in its holder and loose parts, imported with File > Import > USD (add-on enabled) and dropped: Blender's rigid bodies, nothing set by hand. The nuts stay on their threads, the cell stays in its holder. | A nut driven by a torque on an M10 screw: it advances 1.5 mm per turn on the modelled thread, the washer waits below. | An AA cell and a CR2032 carried into their holders and released: the spring plate slides back and the coil compresses, the clip lifts and holds the coin cell. |
All three are rendered in Blender (Cycles) from the generated OpenUSD files:
scripts/physics_gifs.py.
What it does
- Fasteners, metric coarse thread, M3 to M12: hex head screws (ISO 4017, fully threaded, any length), hex nuts (ISO 4032), plain washers (ISO 7089). The visual mesh carries the ISO 68-1 thread profile; the collision is a core cylinder plus one convex piece per 10° of helix (≈ 800 per screw, 200 per nut) with 0.05 mm of radial clearance, so a nut really screws on. Six finishes as a variant set: clear and yellow zinc, black oxide, stainless A2, plain steel, brass.
- Cells and holders: AAA, AA, C, D, CR123A, 18650, 21700 in cradles with a coil spring; the 9 V block in a holder whose terminal slots are keyed; CR2032 and LR44 under a hinged leaf-spring clip. The spring stiffness and wire come from the contact force and preload (k = G d⁴ / 8 D³ n); the clip torsion from the force at its lever arm.
- Variation:
--count Ndistinct instances of each part, with drawn tolerances, defects, wear and physics (see above), and their ground truth. - Related elements on request (
--with): a screw with its nut and washer, a cell with its holder, and a scene with every instance resting on a floor, plus the assemblies (made of instances without defects). - OpenUSD: one file per part, one rigid body per moving part with one visual mesh (so every DCC imports one object per body), mass, centre of mass and principal inertia, convex collision shapes, physics materials, joints with drives, mate frames (thread axis, drive, terminals, cell seat), PhysX attributes for Isaac Sim. A shared material library in UsdPreviewSurface and MaterialX.
- Checks (
quincaillerie sim-test,quincaillerie blender-test): the same scenes, read back from the files, in MuJoCo and in Blender. - A Blender add-on (
quincaillerie blender-addon): rigid bodies and constraints set up on every USD import. --no-physicsfor visual-only assets.
Installation
From PyPI:
pip install "quincaillerie[sim]"
From a clone, for development:
python -m venv .venv && source .venv/bin/activate
pip install -e ".[sim]" # add ",dev" for tests and linting, ",materialx" for the .mtlx file
Requires Python 3.10+. Dependencies: NumPy, SciPy, Pillow and the OpenUSD
bindings (usd-core); MuJoCo for the checks. The Blender checks need Blender
as a Python module (pip install bpy, Python 3.13) or a Blender executable.
Usage
quincaillerie list # sizes, formats, finishes
quincaillerie generate screw M8 --length 30 --with nut,washer --count 100
quincaillerie generate cell 18650 --with holder --count 20 --defect-rate 0.3
quincaillerie generate screw M8 --count 50 --no-vary wear # clean parts, varied fits and defects
quincaillerie generate nut M10 --nominal # the reference part
quincaillerie generate holder 9V --with cell --no-physics # visuals only
quincaillerie generate all --nominal # the whole catalogue (41 assets)
quincaillerie sim-test output/ISO4017_M8x30 --defects # MuJoCo checks (+ crushed threads jam)
quincaillerie blender-test output/catalog # the same in Blender
quincaillerie blender-addon # quincaillerie_physics.zip
python -m quincaillerie ... works too. From Python:
from quincaillerie import generate, resolve
parts = resolve("screw", "M6", length=25, with_=["nut", "washer"])
generate(parts, "output", count=100, defect_rate=0.2) # -> output/ISO4017_M6x25/
generate(parts, "output", vary=("tolerance", "physics")) # clean, undamaged, varied fits
Options of quincaillerie generate
| Option | Default | Effect |
|---|---|---|
KIND KEY |
screw, nut, washer and a size (M3…M12); cell, holder and a format (AA, 18650, 9V, CR2032…); or all |
|
--length L |
per size (M8: 30) | screw length in mm, from 2 pitches to 200 mm |
--with KINDS |
related elements: nut,washer (screw), screw,washer (nut), holder (cell), cell (holder) |
|
--count N |
1 | distinct instances of each part, each with its own drawn variation, all in the scene |
--defect-rate R |
0.15 | share of instances with a defect |
--no-vary ASPECTS |
aspects that stay nominal: tolerance, defect, wear, physics |
|
--nominal |
no variation: the reference part (--count copies in the scene) |
|
--detail |
normal |
visual mesh resolution, low, normal or high (1.4, 3.4 and 7.5 MB for an M8 × 30 screw) |
--finish F |
zinc_clear |
default finish of the fasteners (all six are in every file's variant set) |
--no-physics |
no rigid bodies, colliders, joints or physics scene | |
--no-scene |
assets and materials only | |
--seed S, --out DIR |
1, output |
the same seed and options give the same parts |
Catalogue
| Part | Sizes or formats | Mass (g) | Collision |
|---|---|---|---|
| Hex head screw, ISO 4017 | M3 to M12, any length | M3×12: 0.91 · M8×30: 15.4 · M12×40: 45.2 | core + head + ≈ 800 thread pieces |
| Hex nut, ISO 4032 | M3 to M12 | M3: 0.38 · M8: 5.55 · M12: 15.7 | ≈ 200 pieces (ridge + ring sectors) |
| Plain washer, ISO 7089 | M3 to M12 | M3: 0.12 · M8: 1.81 · M12: 6.19 | 24 ring sectors |
| Cylindrical cell + cradle | AAA, AA, C, D, CR123A, 18650, 21700 | 11.5 to 135 (typical) | can + button cylinders; holder: boxes, spring plate on a prismatic joint |
| 9 V block + keyed holder | 9V (6LR61) | 45 | box + two snap cylinders; terminal slots sized to the snaps |
| Coin / button cell + clip | CR2032, LR44 | 2.9, 1.9 | can + cap cylinders; clip on a revolute joint |
Fastener dimensions (across flats, head height, nut height, washer
diameters, pitch) are the nominal values found in public tables; the ISO
standards themselves were not consulted, and the files say dimensionsStatus = nominal. Cell dimensions are the nominal IEC sizes; masses and contact
forces are typical orders of magnitude. The labels on the cells are generic
("QUINCAILLERIE synthetic test cell"), no brand.
Every cell format, and AA, 18650, 9 V and CR2032 cells seated in their holders (spring compressed, clip lifted), rendered in Blender.
Outputs (under output/<set>/)
| File | Content |
|---|---|
assets/*.usdc, assets/*.usda |
one file per instance, ISO4017_M8x30_007.usdc (Blender: File > Import > USD) |
materials/ |
quincaillerie_materials.usda (UsdPreviewSurface + MaterialX), .mtlx, textures; instances/: the baked textures of worn instances |
scene.usda |
every instance on a floor collider, plus the assemblies |
parts.csv |
one row per instance: mass, collision pieces and every drawn value (variation:clearance_mm, variation:defect, variation:rust, variation:spring_factor…): the ground truth |
summary.json |
counts by kind and by defect, the options used |
sim_test/ |
test scenes and reports (sim-test, blender-test) |
OpenUSD layout
Z up, metres, kilograms; each asset's root prim is its default prim.
/ISO4017_M8x30 Xform, RigidBodyAPI, MassAPI, quincaillerie:* attributes, variant set "finish"
/Visual/ISO4017_M8x30_mesh one mesh, GeomSubset per material
/Collision/core, head, thread_0000 … Cylinder / convexHull meshes, purpose guide
/MateFrame_thread, /MateFrame_drive +Z = screwing axis; hex drive across flats
/Looks, /PhysicsMaterials
/Holder_AA
/base RigidBodyAPI (ABS mass), Visual/Holder_AA_base_mesh, Visual/spring_coil, Collision/…
/spring_plate RigidBodyAPI, Visual, Collision/minus_plate
/Joints/spring PrismaticJoint base -> spring_plate, linear drive = the coil spring (N/m)
The underside of a holder is at z = 0 of the asset: it sits on a floor where
it is placed. Angular drives use UsdPhysics units (per degree). Every
collider has PhysxCollisionAPI offsets and every body PhysxRigidBodyAPI
iterations, read by Isaac Sim and ignored elsewhere.
Blender
Blender's USD importer reads geometry and materials but ignores UsdPhysics: imported as is, a holder is three loose objects and a nut a convex lump that cannot go on a screw. The add-on fixes that once and for all:
quincaillerie blender-addonwritesquincaillerie_physics.zip.- In Blender (4.4 or later): Edit > Preferences > Get Extensions > Install from Disk, pick the zip.
- File > Import > Universal Scene Description, as usual: every body gets its rigid body, every joint its constraint. Press Play.
On import it moves each body's origin to its centre of mass (Blender uses the
origin as centre of mass), makes its collision shape a compound of the USD
shapes (rebuilt from the file, cylinders with 96 sides instead of the
importer's 20), sets mass, friction and small collision margins, turns
joints into Generic Spring constraints with the USD limits, stiffness and
damping, keeps holder coils attached to their spring plate, and sets the
rigid body world to 2000 substeps per second. The same file runs as a script
(blender -b -P quincaillerie_physics.py -- file.usda) without installing.
Bullet takes a spring's rest position from the first frame, so a cell placed already seated in its holder is held by the holder's shape but without the spring's push: it stays seated, it is not pressed.
Checks
quincaillerie sim-test (MuJoCo, CPU) and quincaillerie blender-test
(Blender rigid bodies) build the same small OpenUSD scenes, with the assets
referenced and a floor at z = 0, saved under sim_test/scenes/:
| Check | Scene | Pass when |
|---|---|---|
| drop | each part, tilted, dropped from 4–5 cm | rests on the floor: lowest point within 0.35 mm of it, vertical speed below 1 mm/s (round cells may keep rolling) |
| nut_on_screw | a nut on its screw, both dropped | the nut stays on its thread: shift below half a pitch, off-axis below 0.3 mm |
| holder | an empty holder on the floor | the base settles (0.3 mm) and the plate or clip stays put in the base's frame (0.05 mm) |
| seated | a cell in its holder, on the floor | relative to the holder, the cell stays within the holder's side play (+0.6 mm), the contact moves less than 0.3 mm |
| insertion (MuJoCo) | a cell carried into its holder and released, then the same reversed | seated, spring at the compression this holder should get from this cell (its drawn preload and the cell's drawn length), force within 10 % (coin clips: 15 %, the contact point is estimated); reversed: wrong polarity, or refused by the keying (9 V) |
screwing (MuJoCo, --screwing) |
a nut driven 1 turn/s on a fixed screw, then back | advance per turn within 5 % of the pitch |
jam (MuJoCo, --defects) |
a sound nut driven with a light torque towards the crushed thread of a screw, and the same on a sound screw | it stops at the damage: it travels at least a quarter pitch less than on the sound screw |
With varied instances, nut-on-screw pairs and assemblies are made of instances without defects; the defective ones are dropped like the others, and the crushed threads are checked by the jam test.
Results
quincaillerie generate all --nominal then the checks of the whole
catalogue (41 assets), MuJoCo 3.17 and Blender 5.2 (bpy), one CPU core:
| Check | MuJoCo | Blender (add-on) | Blender, rigid bodies added by hand |
|---|---|---|---|
| drop (31 parts) | 31/31 | 31/31 | 31/31 |
| nut on screw (M3–M12) | 7/7, the nut settles 0.10–0.13 mm (its flank clearance) | 7/7 | 0/7: the nut's convex hull fills the hole and is ejected |
| empty holder (10) | 10/10 | 10/10 | 0/10: base and plate fall apart |
| cell seated in its holder (10) | 10/10 | 10/10 | 0/10 |
| insertion (10 formats, both ways) | 20/20 | ||
| screwing (M3–M12) | 7/7: 0.501, 0.70, 0.80, 0.99–1.005, 1.25, 1.50, 1.75 mm per turn |
A varied batch, quincaillerie generate all --count 5 --defect-rate 0.2 --seed 11 (205 instances, 33 of them with a defect: 9 crushed threads, 11
dished washers, 2 bent screws, 2 burrs, 1 oval nut, 2 dented cans, 6 corroded
terminals), then sim-test --defects and blender-test:
| Check | MuJoCo | Blender (add-on) |
|---|---|---|
| drop (155 parts) | 155/155 | 155/155 |
| nut on screw (2 pairs per size) | 14/14 | 14/14 |
| empty holder (50) | 50/50 | 50/50 |
| cell seated in its holder (50) | 50/50 | 50/50 |
| insertion (50 pairs, both ways) | 100/100, each against its own drawn spring and cell | |
| jam (crushed screws) | 3/3: the nut stops after 0.5–1.2 mm, where it runs 1.5–2.4 mm on a sound screw |
Insertion: every cradle spring compressed to its design within 0.03 mm, with its design force within 1 % (AA: 4.98/5.0 mm, 3.48 N for 3.5 N); the 9 V seats when it is the right way round and is stopped by the terminal slots when reversed; the coin clips press with 2.81 N (CR2032, 3.0 N designed) and 1.82 N (LR44, 2.0 N). The checks of the whole catalogue take about four minutes in MuJoCo and two in Blender; screwing, 1–2 minutes per size.
Limits
- Nominal, not measured. Fastener dimensions come from public tables, not from the standards; cell masses and spring forces are typical. Screws are fully threaded (no plain shank) and have no chamfer under the head.
- Plausible variation, not measured distributions. The ranges (fits, defect sizes, wear, spring scatter) are realistic orders of magnitude, not statistics of a production line. Dents and corrosion crusts are visual only; crushed threads, bends, burrs, oval nuts and dished washers change the collision too.
- Collision in pieces. A thread is ≈ 1000 convex pieces: exact enough to screw, heavy for large scenes (a scene of 100 screws is 100 000 shapes). Loose parts that only need to fall could use a simplified collider; not generated yet.
- Simplified holders. Generic designs (ABS boxes, a flat spring plate), not a product; no electrical contact model.
- Blender springs start at rest. See Blender: a cell placed already seated is not pressed by the spring.
- Isaac Sim not tested here. The files follow the standard
UsdPhysicsschemas and carry PhysX attributes, but have not been loaded in Isaac Sim (no NVIDIA GPU); drive units and contact offsets may need adjusting there.
Roadmap
- Tools that go with the parts: screwdriver bits and screwdrivers, hex keys
and wrenches matched to the drives (
--with tool), and tapped bases. - More fasteners: socket head cap screws (ISO 4762), countersunk and pan head screws, partially threaded shanks, lock and spring washers.
- Simplified colliders for parts that only need to fall; level of detail.
- A PhysX test in Isaac Sim; CAD export (STEP) of the same parametric parts.
- Assembly tasks: torque, misalignment tolerances, success labels.
Development
The project follows GitHub Flow:
mainis always releasable and protected: changes reach it only through pull requests, nobody pushes to it directly.- Every change starts on a short-lived branch from
main, named after its type:feat/<name>,fix/<name>,docs/<name>,ci/<name>, … - Push the branch and open a pull request into
main; CI runs on it. - Merge when CI is green, then delete the branch.
Releasing is a change like any other: on a chore/release-X.Y.Z branch, bump
version in pyproject.toml and move the Unreleased entries of
CHANGELOG.md under the new version; merge the pull request, then tag
vX.Y.Z on main. The release workflow builds the packages and attaches
them, with the Blender add-on, to the GitHub release.
A versioned pre-push hook refuses direct pushes to main. Enable it once per
clone with git config core.hooksPath .githooks (emergency override:
ALLOW_MAIN_PUSH=1 git push).
The README images are made by scripts/: make_logo.py (logo),
readme_images.py (Blender renders and the GitHub social preview
docs/images/social_preview.png, to upload in the repository's Settings >
General > Social preview), physics_gifs.py (the animations).
Continuous integration
| Workflow | When | What |
|---|---|---|
ci.yml |
every push to main, every pull request |
ruff check and ruff format --check; pytest on Python 3.10–3.13 (Linux) and 3.12 (macOS, Windows); a command-line smoke test; the MuJoCo checks of the whole catalogue and an M8 screwing, then varied batches (screws and nuts with crushed threads that must jam, cells and holders with drawn tolerances and springs); the Blender checks of the catalogue and of varied batches through the add-on (and, for comparison, by hand); reports kept as run artifacts; build the wheel and sdist, twine check, install the wheel in a clean environment. |
release.yml |
a pushed tag v* |
checks the tag matches the version in pyproject.toml, tests, builds, then creates the GitHub release with the packages and the Blender add-on attached and the notes taken from CHANGELOG.md. Versions with a, b or rc are marked as pre-releases. |
| Dependabot | monthly | pull requests to update the GitHub Actions and the Python dependencies. |
License
MIT © 2026 Daniel Régis Sarmento Caon
The logo is drawn from DejaVu Sans Bold outlines (Bitstream Vera license), converted to paths, so it displays without the font installed.
Quincaillerie is part of Primvaria, physics-ready variability for Physical AI, in OpenUSD: pip install primvaria installs the whole suite.
Metadata
Release files for quincaillerie 0.1.0b3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| quincaillerie-0.1.0b3.tar.gz | 92.8 kB | With provenance | Details for quincaillerie-0.1.0b3.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| quincaillerie-0.1.0b3-py3-none-any.whl | Python 3 | none | any | Details for quincaillerie-0.1.0b3-py3-none-any.whl |
Total release size: 178.2 kB
Release files / quincaillerie-0.1.0b3.tar.gz
| Download URL | quincaillerie-0.1.0b3.tar.gz |
|---|---|
| Size | 92.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0a3d909be52330f6f9572237cee1f2138cc2729b28ef1f068cbb86bbf0529608
|
|
BLAKE2b-256 checksum How to use checksums |
ee720567f9530c750d86210627e81ec2e43bccee0012f6e8c7fd793513ea1580
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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 Oct 29, 2026.
Transparency logRelease files / quincaillerie-0.1.0b3-py3-none-any.whl
| Download URL | quincaillerie-0.1.0b3-py3-none-any.whl |
|---|---|
| Size | 85.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
99291503e9a47b1cb064a1d67950f9819ac361c1473a17d49a078c6c421b0387
|
|
BLAKE2b-256 checksum How to use checksums |
37848a9b79c4d076e858d05dab2e26cfbdf8a7f6a0dfc95cb4ea3ce9d7b1752d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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 Oct 29, 2026.
Transparency logSecurity for quincaillerie 0.1.0b3
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.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for quincaillerie-0.1.0b3.tar.gz
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 Oct 29, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.