Research & Analysisš¤claudešuniversal
lab-hardware-cad
skills/lab-hardware-cad/SKILL.md
Designs custom laboratory hardware as parametric build123d models and exports fabrication artifacts as STEP, STL, and DXF files - microfluidic chips and molds, optomechanical mounts and breadboard adapters, cuvette and microplate holders, tube racks, animal-behavior rigs, and 3D-printed instrument fixtures. Use when a research task needs a physical part that must mate with standardized labware, an optical table, a cage system, or a printer, CNC, or laser process.
Quick Start
Install this SKILL.md
npx skills add https://github.com/K-Dense-AI/claude-scientific-skills/tree/main/skills/lab-hardware-cad---
name: lab-hardware-cad
description: Designs custom laboratory hardware as parametric build123d models and exports fabrication artifacts as STEP, STL, and DXF files - microfluidic chips and molds, optomechanical mounts and breadboard adapters, cuvette and microplate holders, tube racks, animal-behavior rigs, and 3D-printed instrument fixtures. Use when a research task needs a physical part that must mate with standardized labware, an optical table, a cage system, or a printer, CNC, or laser process.
license: MIT
compatibility: Python 3.11-3.14 with build123d 0.13.0 and matplotlib for snapshots. Geometry commands require build123d; the standards lookup and the interface check run on the standard library alone. Network needed for installation and current vendor drawings; local geometry checks run offline.
allowed-tools: Read Write Edit Bash Glob Grep
metadata:
version: "1.5"
skill-author: K-Dense Inc.
last-reviewed: "2026-10-01"
build123d-version: "0.13.0"
---
# Lab Hardware CAD
Design physical research hardware as **parametric Python source**, export STEP as the
authoritative artifact, and verify the result both numerically and visually before anything
is fabricated.
The hard part of lab hardware is almost never the geometry. It is that the part must mate with
equipment whose dimensions are fixed by a published standard or a vendor drawing. A holder that
is 0.5 mm too wide does not fit the plate reader; a channel with the wrong aspect ratio collapses
during bonding; a mount whose bolt pattern is 25.4 mm instead of 25.0 mm will not reach the
optical table. This skill exists to keep those numbers correct and checked.
## When to use
Use for any request to design, model, or fabricate a physical part for a lab: chip, mold, mount,
adapter, holder, rack, bracket, enclosure, jig, fixture, arena, or maze. Also use to inspect or
modify an existing STEP file.
Do **not** use for finite-element analysis, computational fluid dynamics, molecular structure,
or scientific plotting. Those are different skills.
## Setup
```bash
uv venv --python 3.12 .venv-labcad
uv pip install --python .venv-labcad/bin/python "build123d==0.13.0" "matplotlib>=3.8"
```
build123d 0.13.0 requires Python >=3.11,<3.15 and pulls in the OpenCascade kernel through
`cadquery-ocp-novtk`. The wheel is large; install once per project and reuse it.
All bundled scripts take `--help`. `check.py standards` runs without build123d installed.
**Model files are executed, not parsed.** `gen.py`, `check.py`, and `snapshot.py` import a
`*_model.py` and call its `build()`, which runs arbitrary Python in the current environment. That
is inherent to parametric CAD ā the source is the design. Only run model files authored in this
session or supplied by the user from a trusted location. If a model came from the internet, a
shared drive, or an untrusted colleague, read it before running it and say that you did.
## Required workflow
Follow these steps in order. Steps 5 and 6 are not optional, and step 6 is not waived by step 5
passing.
### 1. Route to a device family
Read the request, classify it, and load **exactly one** family reference. Do not load all four ā
they are long, and mixing conventions between families is a common source of error.
| If the part is | Load |
| --- | --- |
| A chip, mold, channel network, flow cell, gasket, or anything with fluid ports | `references/microfluidics.md` |
| A mount, post, breadboard adapter, cage-system part, filter or sample holder in a beam path | `references/optomechanics.md` |
| An adapter, insert, rack, or holder for plates, cuvettes, tubes, slides, or dishes | `references/labware-adapters.md` |
| An arena, maze, head-fixation part, spout, tether, or extrusion-mounted enclosure for animal work | `references/behavior-rigs.md` |
If the part genuinely spans two families ā a microfluidic chip that bolts to an optical table ā
load the family that owns the **critical interface**, then read only the interface section of the
second. State in your response which family you routed to.
### 2. Establish the interface dimensions before any geometry
Every part has at least one mating interface. Before writing code, write down for each interface:
- the **source** of the dimension: a published standard, a vendor drawing, or a user measurement;
- the **nominal value and tolerance**;
- the **clearance or interference** you intend, and why.
Look the number up in `assets/standards.json` or the family reference. **Never write an interface
dimension from memory.** If the number is not in the standards file or the reference, ask the user
for the vendor drawing or the measurement rather than guessing. A guessed interface dimension is
the single most expensive failure mode in this skill.
A pocket receiving an external component is sized against that component's
**maximum material envelope** ā nominal plus its plus-tolerance ā and only then given clearance.
A shaft entering a hole instead uses the hole's minimum diameter; `envelope` does not model that case.
```bash
python scripts/check.py standards --list
python scripts/check.py standards --show slas-microplate-footprint
```
The bundled standard IDs (exact strings; do not guess variants): `slas-microplate-footprint`,
`slas-microplate-height`, `slas-microplate-flange`, `slas-well-positions-96`,
`slas-well-positions-384`, `slas-well-positions-1536`, `cuvette-standard-10mm`,
`optical-breadboard-metric`, `optical-breadboard-imperial`, `cage-system-30mm`,
`sm1-lens-tube-thread`.
If the part mates with nothing in this list, that is common and fine: declare no interfaces,
and name every interface dimension with its source (user spec, vendor drawing, measurement) as
**unchecked** in the report. Never declare against an unrelated standard to fill the gap ā a
fabricated declaration is worse than an honest "nobody checked this".
### 3. Choose the process before choosing the geometry
Read `references/fabrication-limits.md`. Process determines minimum wall, minimum feature,
achievable tolerance, and whether the part survives autoclaving or contact with your solvent.
Do not assume FDM can hold ±0.05 mm without calibration. Cell-contact SLA parts need
a validated material and post-processing workflow plus assay-specific testing. Record the process and material in the model docstring.
### 4. Author a parametric model
Write `<part>_model.py`. The source is the authoritative artifact ā **never hand-edit an exported
STEP file**, and never regenerate from a mesh.
Requirements:
- Every dimension that a user might change is a **module-level named constant** with units in the
name: `bore_d_mm`, `wall_t_mm`, `post_h_mm`. No bare numbers in the body except 0, 1, and 2.
- Expose `build() -> Part`. `gen.py` calls it.
- Group parameters into an `INTERFACE` block (dimensions fixed by a standard, annotated with the
standard ID) and a `DESIGN` block (dimensions you are free to choose).
- **Derive every computed dimension inside a function**, never at module level, so `--param`
overrides actually reach it.
- Declare an `interfaces()` function returning the dimensions the part must fit, each with its
standard ID and intent. This is what makes the interface machine-checkable in step 5.
`intent` is `"envelope"` when the feature must **accept** any conforming part (a pocket, bore,
or slot ā checked one-sided at maximum material condition plus your clearance) and `"match"`
when comparing against the nominal band, expanded by any explicit allowance.
This is not a conformance certificate. `clearance` is the total intended
clearance in mm and must be non-negative. Declare only dimensions that constrain *this part's
mating features* ā a property of the mating equipment (a table's edge border, a typical plate
thickness) is not an interface of yours. If no bundled standard applies, return `[]`.
- Declare a `checks()` function of **go/no-go gauges measured from the built solid**: a `clear`
region for everything that must pass through or fit in (screw shafts, beam corridors, the
mating part at maximum material condition dropping into its pocket), a `material` region
with a requirement-sized minimum volume for everything that must remain (a ridge, a ledge, a screw seat), and a `bbox_*` bound for every
size limit the user stated. Map **every geometric requirement in the request** to one entry;
these catch the errors that `is_valid`, the bounding box, and declared numbers cannot see.
`gen.py` runs them on every generation and fails the build when one fails. Schema and worked
examples: `references/build123d-patterns.md`.
- Put the process, material, and every interface source in the module docstring.
```python
"""SLAS base-footprint fit coupon; confirm upper plate body/draft before a full holder.
Process: FDM, PETG, 0.2 mm layer. Tolerance budget +/-0.3 mm.
Interfaces:
- Plate pocket: ANSI/SLAS 1-2004 (R2012) footprint 127.76 x 85.48 mm, +/-0.50 overall.
- Upper plate body, lid and stage mounting: not represented in this coupon.
"""
from build123d import *
# --- INTERFACE (fixed by standard; do not tune) ---
plate_l_mm = 127.76 # ANSI/SLAS 1-2004 nominal
plate_w_mm = 85.48 # ANSI/SLAS 1-2004 nominal
plate_tol_mm = 0.50 # ANSI/SLAS 1-2004; the pocket is sized to nominal + this
# --- DESIGN (free) ---
pocket_clearance_mm = 0.40 # per-side; FDM, see fabrication-limits.md
wall_t_mm = 3.0
floor_t_mm = 2.5
body_h_mm = 12.0
def pocket_mm() -> tuple[float, float]:
"""Pocket at the plate's maximum material condition plus clearance per side.
A pocket sized from nominal can jam on conforming plates.
"""
growth = plate_tol_mm + 2 * pocket_clearance_mm
return plate_l_mm + growth, plate_w_mm + growth
def interfaces() -> list[dict]:
"""What this part must fit. `check.py interfaces` verifies every entry."""
pocket_l, pocket_w = pocket_mm()
return [
{"feature": "plate pocket length", "standard": "slas-microplate-footprint",
"dimension": "footprint_length", "value": pocket_l,
"intent": "envelope", "clearance": 2 * pocket_clearance_mm},
{"feature": "plate pocket width", "standard": "slas-microplate-footprint",
"dimension": "footprint_width", "value": pocket_w,
"intent": "envelope", "clearance": 2 * pocket_clearance_mm},
]
def checks() -> list[dict]:
"""Gauges measured from the built solid. Sized from the REQUIREMENT's numbers
(plate MMC, the user's height limit), not from the pocket parameters, so a
wrong parameter cannot shrink the gauge to match the wrong geometry."""
depth = body_h_mm - floor_t_mm
return [
{"feature": "plate at MMC drops into the pocket",
"clear": {"box": (plate_l_mm + plate_tol_mm, plate_w_mm + plate_tol_mm, depth),
"at": [(0.0, 0.0, floor_t_mm + depth / 2)]}},
{"feature": "under 15 mm for the stage", "bbox_z": {"max": 15.0}},
]
def build() -> Part:
pocket_l, pocket_w = pocket_mm()
with BuildPart() as carrier:
Box(pocket_l + 2 * wall_t_mm, pocket_w + 2 * wall_t_mm, body_h_mm,
align=(Align.CENTER, Align.CENTER, Align.MIN))
with Locations((0, 0, floor_t_mm)):
Box(pocket_l, pocket_w, body_h_mm, mode=Mode.SUBTRACT,
align=(Align.CENTER, Align.CENTER, Align.MIN))
return carrier.part
```
See `references/build123d-patterns.md` for the builder-vs-algebra choice, the `interfaces()`
contract, sketching, selectors, fillets, and threaded-insert bores.
### 5. Generate and run the checks
```bash
python scripts/gen.py carrier_model.py --outdir out/
python scripts/check.py facts out/carrier.step
python scripts/check.py interfaces out/carrier.manifest.json
python scripts/check.py geometry out/carrier.step --model carrier_model.py
```
`gen.py` also evaluates the model's `checks()` gauges against the solid it just built, prints
each PASS/FAIL, records them in the manifest, and exits non-zero on a failure ā so a part that
violates its own declared geometry never silently becomes an artifact. `check.py geometry`
re-runs the same gauges against the exported STEP. Repeat any `gen.py --param` overrides
with `check.py geometry --param`; otherwise the gauges use the source defaults.
`out/` is a scratch convention, not a requirement. When the user asked for deliverables in a
specific place, generate there (`--outdir .`) or copy the STEP, manifest, and DXF to it before
finishing ā a deliverable that exists only inside `out/` has not been delivered.
`gen.py` writes `carrier.step` (authoritative), `carrier.stl` (mesh preview and printing), and
`carrier.manifest.json` recording the source hash, resolved parameters, declared interfaces,
library versions, and measured bounding box, volume, and validity. The manifest is the provenance
record ā keep it with the artifact.
`check.py facts` reports `is_valid`, bounding box, volume, surface area, centre of mass, and
solid count. A part that reports `is_valid: false` is broken geometry; fix the source before going
further.
`check.py interfaces` evaluates every entry the model declared against the standards database
and exits non-zero on failure. **Be clear about what it does and does not verify:** it checks the
*declared numbers* ā catching a transcribed dimension, the wrong standard, and
nominal-instead-of-MMC sizing ā but it never measures the built geometry, and a value computed
from the same constants it is checked against passes with zero headroom by construction. Do not
cite it as evidence the geometry is right; `facts` and the snapshot are the geometry checks.
An empty declaration list passes: a part that mates with nothing in the bundled database has
nothing to declare, and its interface dimensions are instead named as unchecked in the report.
A manifest must contain an explicit `interfaces` list; a missing field or `null` is a malformed
manifest, not evidence that interfaces were reviewed and none applied.
Use `interfaces` rather than `check.py fit` for anything internal ā a pocket, bore, or slot does
not appear in the part's outer bounding box, which is what `fit` measures. Reach for `fit` only
to check one number by hand (`--value footprint_length=129.06`), or when the part's own outline
is the interface, such as a gasket cut to a plate footprint.
For assemblies, check that parts do not interfere:
```bash
python scripts/check.py clearance out/carrier.step out/lid.step --min 0.3
```
### 6. Snapshot and actually look at it
```bash
python scripts/snapshot.py out/carrier.step --out out/carrier.png
```
Then **read the PNG**. This step is mandatory after every generation and every modification.
Deterministic checks passing is not a reason to skip it: `is_valid` and a correct bounding box are
both fully consistent with a pocket cut on the wrong face, a boss placed outside the body, or a
fillet that ate a feature. Those errors are obvious in a picture and invisible in the numbers.
Know the render's limits too. A feature much smaller than the frame ā a 0.3 mm mold ridge on a
40 mm part, a counterbore step on a plate ā may not be decidable from the views at all. Do not
report seeing something the image cannot resolve; that is worse than not looking. For such
features the skill has instruments: `check.py bores` prints every cylindrical face (diameter,
axis, position, span, sweep) so you can reconcile the drilling against the model's intent, and
`check.py probe` answers a one-off "is this region clear / is material present here" without
editing the model. Cite the measured numbers; report from the picture only what the picture
actually shows.
The six views are true orthographic projections, and the outlines are the model's real edges drawn
**without hidden-line removal**. So a circle visible "through" material is a bore on the far side,
not a window ā the part is not transparent. Read it that way rather than reporting a hole that
is not there.
State in your response what you saw in the snapshot, not merely that you generated one.
### 7. Repair through the source
If any check fails, edit the parameters or the model code, rerun `gen.py`, and rerun **both**
step 5 and step 6. Never patch the STEP.
### 8. Report before fabrication
Work through `references/validation.md` and give the user: the process and material, every
interface dimension with its source and tolerance, the clearances chosen, what the snapshot showed,
and any check that did not pass.
Flag explicitly every interface the automatic check could not cover ā a vendor drawing, a user
measurement, a standard not in the bundled database. `check.py interfaces` reports only what the
model declared against a known standard, so silence there is not confirmation; a dimension nobody
could check has to be named as such.
## Units
build123d is unitless internally. Geometry uses **millimetres and degrees**; mesh
`angular_tolerance` uses **radians** (0.1 rad ā 5.7°).
`export_step` is called with `Unit.MM`. Imperial hardware appears throughout optomechanics
(1/4-20 screws, 1 inch grids, SM1 threads); convert to millimetres in a single named constant at
the point of definition and never mix systems inside an expression. 1 inch is exactly 25.4 mm, and
a 25 mm metric optical grid is **not** interchangeable with a 1 inch imperial grid ā the error
accumulates to 1.6 mm over four pitches (five hole centres).
## Tolerances and fits
A nominal dimension is not a fit. Every mating dimension needs a deliberate clearance chosen from
the process tolerance in `references/fabrication-limits.md`. Common defaults, per side:
| Fit | FDM | SLA | CNC |
| --- | --- | --- | --- |
| Free-sliding (plate in a pocket) | 0.40 mm | 0.20 mm | 0.10 mm |
| Located but removable | 0.25 mm | 0.10 mm | 0.05 mm |
| Press / interference | Coupon-specific | Coupon-specific | Toleranced fit design |
Budget the receiving part's worst-case undersize separately: minimum actual pocket
size must exceed the mating part's maximum size plus the required functional gap.
These are starting points for a first article, not guarantees. Say so when you report them, and
recommend printing a test coupon of the critical interface before committing to a full part.
## Scientific caveats
- **Material compatibility governs.** A geometrically perfect part in the wrong polymer fails in
service: autoclave cycles distort PLA, many solvents craze acrylic, and uncured SLA resin is
cytotoxic. Check `references/fabrication-limits.md` before recommending a material for anything
contacting cells, tissue, solvents, or heat.
- **Optical parts have non-geometric requirements.** Autofluorescence, surface roughness, and
stray-light scatter are not visible in a STEP file. Black resin is not automatically low-scatter.
- **Vendor labware varies.** The SLAS standards fix the plate footprint but not well geometry,
skirt profile, or lid fit, and consumable tubes differ between suppliers. Design to the standard
where one exists; otherwise require a measurement.
- **A passing bounding box is not a passing part.** `fit` checks the dimensions it is given. It
cannot see a missing feature, and it does not replace the snapshot.
## References
| File | Contents |
| --- | --- |
| `references/microfluidics.md` | Channel cross-sections and aspect ratios, mold vs chip polarity, minimum features by process, port and tubing interfaces, bonding lands, dead volume |
| `references/optomechanics.md` | Breadboard grids and screw clearances, post and pedestal heights, 30 mm cage geometry, SM lens-tube threads, beam height |
| `references/labware-adapters.md` | ANSI/SLAS 1-4 microplate dimensions, cuvettes, tubes, slides, dishes, deck and stage constraints |
| `references/behavior-rigs.md` | Arena and maze geometry, head-fixation interfaces, spouts and ports, T-slot extrusion, cleaning and durability |
| `references/fabrication-limits.md` | Process tolerances, minimum walls and features, clearance and thread inserts, materials, autoclav
⦠(truncated)Repository
48.8kstars
4.4kforks
Details
Related Skills
More research & analysis skills you might like
pr-improver
Research & AnalysisRuns an autonomous review-and-fix improvement loop over the current branch's changes until a PR review comes back clean, scoped mechanically to the directories the branch touched. Reviews are performed by an installed PR-review skill (default: pr-review-toolkit's review-pr). Use to fix review findings on a branch before opening or updating a pull request ('clean up this branch', 'fix this PR until review passes', 'run review-and-fix on my changes'). NOT for a one-time review ā run the PR-review skill directly.
š¤claudešuniversal
7.5k
643
View
code-improver
Research & AnalysisRuns an autonomous review-and-fix improvement loop over any code target ā a skill, plugin, module, or directory ā using a reviewer the user names: any installed skill or agent. Keeps a cross-round findings ledger, escalates when fixes stop converging, and guards scope mechanically. Use when asked to 'improve this code until review passes', 'run an improvement loop with <reviewer>', or to iterate review-and-fix with a specific reviewer. For skills prefer the skill-improver entry; for a branch prefer pr-improver.
š¤claudešuniversal
7.5k
643
View
skill-improver
Research & AnalysisRuns an autonomous review-and-fix improvement loop over a Claude Code skill until a review comes back clean, with a cross-round findings ledger, escalation when fixes stop converging, and a mechanical scope guard. Reviews are performed by the plugin-dev skill-reviewer agent. Use to fix skill quality issues, iteratively refine a skill, or resume a loop after an escalation ('fix my skill', 'improve this skill until it passes review', 'skill improvement loop'). NOT for a one-time review ā use the plugin-dev skill-reviewer agent directly.
š¤claudešuniversal
7.5k
643
View
audit-augmentation
Research & AnalysisAugments Trailmark code graphs with external audit findings from SARIF static analysis results, weAudit annotation files, and version-gated Trailmark 0.4.x binary-analysis graph exports. Maps findings to graph nodes by file and line overlap, creates severity-based subgraphs, and enables cross-referencing findings with pre-analysis data (blast radius, taint, etc.). Use when projecting SARIF results onto a code graph, overlaying weAudit annotations, importing binary graph findings, cross-referencing Semgrep, CodeQL, or binary-analysis findings with call graph data, or visualizing audit findings in the context of code structure.
šuniversal
7.5k
643
View