[Upstream sync] K-Dense-AI/scientific-agent-skills (github) — 0 added, 8 modified #45

Merged
promptadmin merged 8 commits from upstream-sync/scientific-agent-skills-20260816-336c4f-xhnj into main 2026-08-21 14:39:31 +00:00
8 changed files with 1813 additions and 0 deletions
@@ -0,0 +1,378 @@
---
lineage_type: import
upstream_source: https://github.com/K-Dense-AI/scientific-agent-skills/blob/336c4f83/skills/lab-hardware-cad/SKILL.md
upstream_sha: 336c4f83
imported_at: 2026-08-16
prompt_class: catalogue
upstream_changes: accepted
name: lab-hardware-cad
description: Design custom laboratory hardware as parametric build123d models and export fabrication-ready 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.10-3.14 with build123d 0.11.1 and matplotlib for snapshots. Geometry commands require build123d; the standards lookup and the interface check run on the standard library alone. No network access needed.
allowed-tools: Read Write Edit Bash Glob Grep
metadata:
version: "1.2"
skill-author: K-Dense Inc.
last-reviewed: "2026-08-15"
build123d-version: "0.11.1"
---
# 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.11.1" "matplotlib>=3.8"
```
build123d 0.11.1 requires Python >=3.10,<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 feature that must *receive* a standardised component is sized against that component's
**maximum material condition** — nominal plus its plus-tolerance — and only then given clearance.
Sized from nominal instead, it fits only the smaller half of conforming parts.
```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.
FDM cannot hold ±0.05 mm; SLA resin is generally not safe for cell contact without post-cure and
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 this part must itself conform (symmetric band). `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 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 microplate carrier for a custom stage insert.
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.25.
- Stage bolts: user-measured, 40.0 mm centres (drawing in docs/stage.pdf).
"""
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.25 # 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 jams on roughly half of 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, which is the authoritative artifact.
`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.
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=128.81`), 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 and everything in this skill is **millimetres and degrees**.
`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 holes.
## 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 | -0.05 mm | -0.03 mm | -0.02 mm |
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, autoclave and solvent and biocompatibility |
| `references/validation.md` | Pre-fabrication checklist and the failure modes each item catches |
| `references/build123d-patterns.md` | build123d 0.11.1 API cookbook: builder vs algebra, sketches, selectors, joints, exports |
## Scripts
| Command | Purpose |
| --- | --- |
| `gen.py <model.py> --outdir DIR` | Run `build()`, export STEP and STL, write the provenance manifest |
| `gen.py <model.py> --dxf [--dxf-z MM]` | Also slice a 2D DXF profile for laser cutting (default plane: mid-height) |
| `check.py facts <step>` | Validity, bounding box, volume, area, centre of mass, solid count |
| `check.py interfaces <manifest\|model.py>` | Check every declared interface number against its standard; non-zero exit on failure |
| `check.py geometry <model.py\|step --model M>` | Evaluate the model's `checks()` gauges against the built solid — measured, not declared |
| `check.py probe <step> --cyl D\|--box X,Y,Z --at ...` | One ad-hoc gauge: is this region clear of material, or filled with it |
| `check.py bores <step>` | Census of every cylindrical face: diameter, axis, position, span, sweep |
| `check.py fit --standard ID --value DIM=MM` | Check one dimension by hand, or a part whose outer envelope is the interface |
| `check.py clearance <a> <b> --min MM` | Minimum distance between two solids; detects interference |
| `check.py standards [--list\|--show ID]` | Browse the bundled standards data (standard library only) |
| `snapshot.py <step> --out PNG` | Six-view orthographic and isometric render for visual review |
All commands accept `--json` for machine-readable output and write progress to stderr.
`check.py standards`, and `check.py interfaces` on a manifest, run without build123d installed.
@@ -0,0 +1,211 @@
---
title: "Standards"
task: ""
lineage_type: import
upstream_source: https://github.com/K-Dense-AI/scientific-agent-skills/blob/336c4f83/skills/lab-hardware-cad/assets/standards.json
upstream_sha: 336c4f83
imported_at: 2026-08-16
prompt_class: unknown
upstream_changes: accepted
author: upstream
validated: false
---
{
"schema_version": "1.0",
"units": "mm",
"note": "Dimensional standards for lab-hardware interfaces. Every entry carries a source. Entries with verified=false were not confirmed against the primary document during authoring and must be checked before use.",
"last_reviewed": "2026-08-15",
"standards": {
"slas-microplate-footprint": {
"title": "Microplate footprint (base outline)",
"authority": "ANSI/SLAS",
"document": "ANSI/SLAS 1-2004 (R2012) Footprint Dimensions",
"url": "https://www.slas.org/SLAS/assets/File/public/standards/ANSI_SLAS_1-2004_FootprintDimensions.pdf",
"verified": true,
"dimensions": {
"footprint_length": {
"nominal": 127.76,
"tol_plus": 0.25,
"tol_minus": 0.25,
"note": "Measured within 12.7 mm of the outside corners. Relaxes to +/-0.5 mm at any point along the side."
},
"footprint_width": {
"nominal": 85.48,
"tol_plus": 0.25,
"tol_minus": 0.25,
"note": "Measured within 12.7 mm of the outside corners. Relaxes to +/-0.5 mm at any point along the side."
},
"corner_radius": {
"nominal": 3.18,
"tol_plus": 1.6,
"tol_minus": 1.6,
"note": "Outside radius of the four bottom-flange corners (convex). A receiving pocket's internal fillet must be no LARGER than the minimum (1.58) or it bulges into the plate corner and binds; a sharp pocket corner or a corner-relief cut always clears. Do not size the pocket fillet to the maximum radius."
}
},
"fit_checks": [
{"measure": "bbox_x", "dimension": "footprint_length"},
{"measure": "bbox_y", "dimension": "footprint_width"}
],
"design_note": "For a pocket that receives a plate, add clearance per side on top of the maximum material condition (127.76 + 0.25 = 128.01). Check the pocket, not the plate."
},
"slas-microplate-height": {
"title": "Microplate height",
"authority": "ANSI/SLAS",
"document": "ANSI/SLAS 2-2004 (R2012) Height Dimensions",
"url": "https://www.slas.org/SLAS/assets/File/public/standards/ANSI_SLAS_2-2004_HeightDimensions.pdf",
"verified": true,
"dimensions": {
"plate_height": {
"nominal": 14.35,
"tol_plus": 0.25,
"tol_minus": 0.25,
"note": "Datum A (resting plane) to the maximum protrusion of the perimeter wells. Secondary sources also quote +/-0.76 mm; consult the document before relying on the tighter band. Lidded and deep-well plates are taller and out of scope of this dimension."
}
},
"fit_checks": [
{"measure": "bbox_z", "dimension": "plate_height"}
]
},
"slas-microplate-flange": {
"title": "Microplate bottom outside flange height",
"authority": "ANSI/SLAS",
"document": "ANSI/SLAS 3-2004 (R2012) Bottom Outside Flange Dimensions",
"url": "https://www.slas.org/SLAS/assets/File/public/standards/ANSI_SLAS_3-2004_BottomOutsideFlangeDimensions.pdf",
"verified": true,
"dimensions": {
"flange_height_short": {"nominal": 2.41, "tol_plus": 0.38, "tol_minus": 0.38},
"flange_height_medium": {"nominal": 6.10, "tol_plus": 0.38, "tol_minus": 0.38},
"flange_height_tall": {"nominal": 7.62, "tol_plus": 0.38, "tol_minus": 0.38}
},
"fit_checks": [],
"design_note": "Three flange heights are standardised. A gripper or carrier that assumes one will drop plates built to another. Ask which the user has."
},
"slas-well-positions-96": {
"title": "96-well plate well positions",
"authority": "ANSI/SLAS",
"document": "ANSI/SLAS 4-2004 (R2012) Well Positions",
"url": "https://www.slas.org/SLAS/assets/File/public/standards/ANSI_SLAS_4-2004_WellPositions.pdf",
"verified": true,
"dimensions": {
"well_pitch": {"nominal": 9.0, "tol_plus": 0.0, "tol_minus": 0.0, "note": "Centre-to-centre in both x and y."},
"a1_offset_x": {"nominal": 14.38, "tol_plus": 0.0, "tol_minus": 0.0, "note": "Left outside edge to the centre of column 1."},
"a1_offset_y": {"nominal": 11.24, "tol_plus": 0.0, "tol_minus": 0.0, "note": "Top outside edge to the centre of row A."},
"well_position_tolerance": {"nominal": 0.70, "tol_plus": 0.0, "tol_minus": 0.0, "note": "Each well centre lies within a 0.70 mm diameter of nominal. This is a positional tolerance zone, not a +/- band."}
},
"fit_checks": [],
"design_note": "Grid layout: x = a1_offset_x + 9.0 * column_index, y = a1_offset_y + 9.0 * row_index, measured from the plate outline corner."
},
"slas-well-positions-384": {
"title": "384-well plate well positions",
"authority": "ANSI/SLAS",
"document": "ANSI/SLAS 4-2004 (R2012) Well Positions",
"url": "https://www.slas.org/SLAS/assets/File/public/standards/ANSI_SLAS_4-2004_WellPositions.pdf",
"verified": false,
"dimensions": {
"well_pitch": {"nominal": 4.5, "tol_plus": 0.0, "tol_minus": 0.0, "note": "Centre-to-centre in both x and y."},
"a1_offset_x": {"nominal": 12.13, "tol_plus": 0.0, "tol_minus": 0.0, "note": "UNVERIFIED. Derived as the 96-well offset minus half the 96-well pitch. Confirm against ANSI/SLAS 4-2004 before cutting metal."},
"a1_offset_y": {"nominal": 8.99, "tol_plus": 0.0, "tol_minus": 0.0, "note": "UNVERIFIED. Derived, as above. Confirm against the document."}
},
"fit_checks": [],
"design_note": "Only well_pitch is confirmed here. Read the standard for the A1 offsets before relying on them."
},
"slas-well-positions-1536": {
"title": "1536-well plate well positions",
"authority": "ANSI/SLAS",
"document": "ANSI/SLAS 4-2004 (R2012) Well Positions",
"url": "https://www.slas.org/SLAS/assets/File/public/standards/ANSI_SLAS_4-2004_WellPositions.pdf",
"verified": false,
"dimensions": {
"well_pitch": {"nominal": 2.25, "tol_plus": 0.0, "tol_minus": 0.0, "note": "Centre-to-centre in both x and y."},
"a1_offset_x": {"nominal": 11.005, "tol_plus": 0.0, "tol_minus": 0.0, "note": "UNVERIFIED. Derived from the 96-well offset. Confirm against ANSI/SLAS 4-2004."},
"a1_offset_y": {"nominal": 7.865, "tol_plus": 0.0, "tol_minus": 0.0, "note": "UNVERIFIED. Derived, as above. Confirm against the document."}
},
"fit_checks": [],
"design_note": "Only well_pitch is confirmed here."
},
"cuvette-standard-10mm": {
"title": "Standard 10 mm path-length spectrophotometer cuvette",
"authority": "De facto industry convention",
"document": "No single ANSI/ISO document fixes this; it is a near-universal convention across suppliers.",
"url": "https://spectrecology.com/blog/guide-to-cuvettes/",
"verified": true,
"dimensions": {
"external_width": {"nominal": 12.5, "tol_plus": 0.1, "tol_minus": 0.1, "note": "Tolerance is indicative; suppliers vary."},
"external_depth": {"nominal": 12.5, "tol_plus": 0.1, "tol_minus": 0.1},
"external_height": {"nominal": 45.0, "tol_plus": 0.5, "tol_minus": 0.5, "note": "Body height excluding any cap or stopper. Semi-micro and micro cuvettes share the external footprint but differ in height and internal geometry."},
"path_length": {"nominal": 10.0, "tol_plus": 0.0, "tol_minus": 0.0, "note": "Internal optical path. 12.5 external minus 2 x 1.25 mm wall."},
"wall_thickness": {"nominal": 1.25, "tol_plus": 0.0, "tol_minus": 0.0}
},
"fit_checks": [
{"measure": "bbox_x", "dimension": "external_width"},
{"measure": "bbox_y", "dimension": "external_depth"}
],
"design_note": "Because this is a convention rather than a standard, a holder should be designed with generous clearance or a compliant feature. Confirm against the user's actual cuvettes."
},
"optical-breadboard-metric": {
"title": "Metric optical breadboard hole grid",
"authority": "De facto industry convention",
"document": "Universal across Thorlabs, Newport, Edmund and others for metric tables.",
"url": "https://www.thorlabs.com/imperial-and-metric-threading",
"verified": true,
"dimensions": {
"grid_pitch": {"nominal": 25.0, "tol_plus": 0.0, "tol_minus": 0.0, "note": "Metric grid. NOT interchangeable with the 25.4 mm imperial grid."},
"thread": {"nominal": 6.0, "tol_plus": 0.0, "tol_minus": 0.0, "note": "M6 x 1.0 tapped holes."},
"clearance_hole_close": {"nominal": 6.4, "tol_plus": 0.0, "tol_minus": 0.0, "note": "Close-fit clearance for an M6 cap screw."},
"clearance_hole_normal": {"nominal": 6.6, "tol_plus": 0.0, "tol_minus": 0.0, "note": "Normal-fit clearance for M6. Prefer this on printed parts."},
"counterbore_dia": {"nominal": 11.0, "tol_plus": 0.0, "tol_minus": 0.0, "note": "For an M6 socket head cap screw head (nominal head dia 10 mm)."},
"screw_head_height": {"nominal": 6.0, "tol_plus": 0.0, "tol_minus": 0.0, "note": "M6 socket head cap screw head height (ISO 4762). A counterbore shallower than this leaves the head proud, not flush."},
"border": {"nominal": 12.5, "tol_plus": 0.0, "tol_minus": 0.0, "note": "Typical edge-to-first-hole distance. A property of the TABLE, not of your part: your plate's edge margin is a free design choice, so do not declare this as an interface."}
},
"fit_checks": [],
"design_note": "Slot rather than hole one of any pair of mounting features to absorb grid and print tolerance."
},
"optical-breadboard-imperial": {
"title": "Imperial optical breadboard hole grid",
"authority": "De facto industry convention",
"document": "Universal across Thorlabs, Newport, Edmund and others for imperial tables.",
"url": "https://www.thorlabs.com/imperial-and-metric-threading",
"verified": true,
"dimensions": {
"grid_pitch": {"nominal": 25.4, "tol_plus": 0.0, "tol_minus": 0.0, "note": "1 inch exactly. Over four holes this differs from the metric grid by 1.6 mm."},
"thread_major_dia": {"nominal": 6.35, "tol_plus": 0.0, "tol_minus": 0.0, "note": "1/4-20 UNC: 0.25 inch major diameter, 20 threads per inch."},
"clearance_hole_normal": {"nominal": 6.8, "tol_plus": 0.0, "tol_minus": 0.0, "note": "Normal-fit clearance for a 1/4-20 screw."},
"counterbore_dia": {"nominal": 11.2, "tol_plus": 0.0, "tol_minus": 0.0, "note": "For a 1/4-20 socket head cap screw head."},
"screw_head_height": {"nominal": 6.35, "tol_plus": 0.0, "tol_minus": 0.0, "note": "1/4-20 socket head cap screw head height (0.25 inch). A counterbore shallower than this leaves the head proud, not flush."},
"border": {"nominal": 12.7, "tol_plus": 0.0, "tol_minus": 0.0, "note": "Typical 0.5 inch edge-to-first-hole distance. A property of the TABLE, not of your part: your plate's edge margin is a free design choice, so do not declare this as an interface."}
},
"fit_checks": [],
"design_note": "Ask which table the user has. Assuming the wrong system is the most common optomechanical design error."
},
"cage-system-30mm": {
"title": "30 mm cage system",
"authority": "Thorlabs (de facto standard, second-sourced by others)",
"document": "Thorlabs 30 mm cage system construction rods and cage plates",
"url": "https://www.thorlabs.com/newgrouppage9.cfm?objectgroup_ID=2273",
"verified": true,
"dimensions": {
"rod_spacing": {"nominal": 30.0, "tol_plus": 0.0, "tol_minus": 0.0, "note": "Rod centre to rod centre, on a square pattern. 1.18 inch."},
"rod_diameter": {"nominal": 6.0, "tol_plus": 0.0, "tol_minus": 0.0, "note": "ER series cage rods."},
"plate_thickness_typical": {"nominal": 8.9, "tol_plus": 0.0, "tol_minus": 0.0, "note": "0.35 inch, the CP33 standard cage plate. Informational, not a mating dimension: custom plates may be any thickness, but matching it keeps optical path budgets simple."}
},
"fit_checks": [],
"design_note": "The 30 mm rod square is centred on the optical axis. A custom plate must place its aperture at the centroid of the four rod bores. Bore diameter is rod_diameter plus twice the free-sliding per-side clearance for YOUR process (fabrication-limits.md): about 6.2 CNC, 6.4 SLA, 6.8 FDM. 6.1 is a reamed-metal number and binds on printed parts. Four bores on a common square over-constrain each other, so do not go tighter than the fits table. Declare the bore against rod_diameter with intent envelope and the clearance you chose."
},
"sm1-lens-tube-thread": {
"title": "SM1 lens tube thread",
"authority": "Thorlabs (de facto standard)",
"document": "Thorlabs SM1 series threading",
"url": "https://www.thorlabs.com/newgrouppage9.cfm?objectgroup_id=4114",
"verified": true,
"dimensions": {
"thread_major_dia": {"nominal": 26.289, "tol_plus": 0.0, "tol_minus": 0.0, "note": "1.035 inch-40 thread. Holds 1 inch diameter optics."},
"threads_per_inch": {"nominal": 40.0, "tol_plus": 0.0, "tol_minus": 0.0},
"pitch": {"nominal": 0.635, "tol_plus": 0.0, "tol_minus": 0.0, "note": "25.4 / 40 mm."},
"optic_dia": {"nominal": 25.4, "tol_plus": 0.0, "tol_minus": 0.0, "note": "1 inch optic."}
},
"fit_checks": [],
"design_note": "A 40 TPI thread has a 0.635 mm pitch, which is at or below the resolution of most FDM printers. Print a clearance bore and use a purchased SM1 adapter or a tapped insert rather than printing the thread."
}
}
}
@@ -0,0 +1,376 @@
---
title: "build123d 0.11.1 patterns"
task: ""
lineage_type: import
upstream_source: https://github.com/K-Dense-AI/scientific-agent-skills/blob/336c4f83/skills/lab-hardware-cad/references/build123d-patterns.md
upstream_sha: 336c4f83
imported_at: 2026-08-16
prompt_class: unknown
upstream_changes: accepted
author: upstream
validated: false
---
# build123d 0.11.1 patterns
An API cookbook for the geometry this skill actually needs. Every snippet here was run against
build123d 0.11.1 on Python 3.12.
## Builder mode or algebra mode
build123d offers two equivalent APIs.
```python
# Builder mode: a context manager collects operations. mode= controls the boolean.
with BuildPart() as ex:
Box(80.0, 60.0, 10.0)
Cylinder(radius=11.0, height=10.0, mode=Mode.SUBTRACT)
part = ex.part
# Algebra mode: plain objects and operators.
part = Box(80.0, 60.0, 10.0) - Cylinder(radius=11.0, height=10.0)
```
**Use builder mode for parts in this skill.** Selectors (`ex.edges()`, `ex.faces()`) read naturally
from the builder, which is what you need for fillets and for placing features on found faces.
Algebra mode is a good fit for short, purely constructive shapes.
Do not mix the two styles inside one `build()`.
## The model file contract
`gen.py` imports the module, calls `build()`, and then reads `interfaces()`. Parameters must be
module-level so they can be overridden with `--param`.
```python
"""One-line description of the part.
Process: SLA, tough resin. Orientation: bore axis vertical.
Interfaces:
- Rod bores: 30 mm cage system, Thorlabs ER series (cage-system-30mm).
"""
from build123d import *
# --- INTERFACE (fixed; do not tune) ---
rod_spacing_mm = 30.0 # cage-system-30mm
rod_bore_d_mm = 6.4 # rod_diameter 6.0 + 2 x 0.20 SLA free-sliding (fabrication-limits.md)
# --- DESIGN (free) ---
plate_t_mm = 8.9
aperture_d_mm = 25.4
def interfaces() -> list[dict]:
return [
{"feature": "cage rod bore spacing", "standard": "cage-system-30mm",
"dimension": "rod_spacing", "value": rod_spacing_mm, "intent": "match"},
{"feature": "cage rod bore diameter", "standard": "cage-system-30mm",
"dimension": "rod_diameter", "value": rod_bore_d_mm,
"intent": "envelope", "clearance": 0.4},
]
def build() -> Part:
half = rod_spacing_mm / 2
with BuildPart() as plate:
Box(rod_spacing_mm + 12.0, rod_spacing_mm + 12.0, plate_t_mm)
with Locations((half, half), (-half, half), (half, -half), (-half, -half)):
Hole(radius=rod_bore_d_mm / 2)
Hole(radius=aperture_d_mm / 2)
return plate.part
```
## Declaring interfaces
Most lab-hardware interfaces are **internal features** — a pocket, a bore, a slot — and none of
them appear in the part's outer bounding box. So `check.py fit` cannot find them by measuring the
STEP, and hand-copying the number into `--value` reintroduces exactly the transcription error the
skill exists to prevent. Declaring them closes the loop: `gen.py` records the declaration in the
manifest, and `check.py interfaces` verifies every entry.
Each entry needs `standard`, `dimension`, and `value`; `feature`, `intent`, and `clearance` are
optional:
| Key | Meaning |
| --- | --- |
| `standard` | ID from `check.py standards --list` |
| `dimension` | a dimension name inside that standard |
| `value` | the number **this model computed**, in mm |
| `feature` | human label for the check output (default: the dimension name) |
| `intent` | `match` if this part must itself conform; `envelope` if the feature must accept any conforming part (default: `match`) |
| `clearance` | total intended clearance in mm, both sides (default: 0) |
**Write `interfaces()` as a function, and compute derived dimensions inside functions.** A
module-level `INTERFACES = [...]` list is also accepted, but it is evaluated at import — before
`--param` is applied — so any value derived from an overridden parameter is recorded wrong. The same
applies to the geometry: derive inside `build()` or a helper, never at module level.
```python
# Wrong: --param plate_tol_mm=0 silently leaves pocket_l_mm at the old value
pocket_l_mm = plate_l_mm + plate_tol_mm + 2 * pocket_clearance_mm
# Right: recomputed on every call, so overrides land
def pocket_l_mm() -> float:
return plate_l_mm + plate_tol_mm + 2 * pocket_clearance_mm
```
`gen.py` warns when it sees a static `INTERFACES` list together with `--param`.
## Declaring geometry checks
`interfaces()` compares declared numbers against the standards database; it never touches the
solid. `checks()` is its measured counterpart: a list of **go/no-go gauges** evaluated by boolean
intersection against the part `build()` actually produced. `gen.py` runs them on every
generation and fails the build if one fails; `check.py geometry` re-runs them against an
exported STEP.
The principle: **every geometric requirement in the request maps to one entry.** Something must
pass through (a screw, a beam, a probe) → a `clear` region. Something must fit into a void (a
plate into a pocket) → a `clear` box the size of the mating part at maximum material condition.
Something must remain (a ridge, a ledge, a screw seat) → a `material` region. A stated size
limit → a `bbox_*` bound. These are exactly the errors `is_valid`, the bounding box, and a
declared-number check cannot see.
```python
def checks() -> list[dict]:
top = plate_t_mm / 2
return [
# a clear region: no material may intrude (screw shafts, through the part)
{"feature": "M6 screws pass all four bores",
"clear": {"cylinder": 6.0, "axis": "z", "at": bolt_xy()}},
# a keep-out with an explicit span (a beam corridor along x at height z)
{"feature": "beam clear at 15 mm above the bench",
"clear": {"cylinder": 5.0, "axis": "x", "at": [(0.0, 15.0)]}},
# a gauge part that must drop into a pocket: the mating part at MMC
{"feature": "SLAS plate at MMC drops into the pocket",
"clear": {"box": (128.01, 85.73, pocket_depth_mm()),
"at": [(0.0, 0.0, floor_t_mm + pocket_depth_mm() / 2)]}},
# a counterbore that really is a counterbore: recess open, seat present.
# The second entry is what catches a recess that punched through.
{"feature": "counterbore recess open at the top",
"clear": {"cylinder": cbore_d_mm - 0.2, "axis": "z", "at": bolt_xy(),
"span": (top - cbore_depth_mm + 0.1, top + 0.1)}},
{"feature": "screw seat present below the recess",
"material": {"cylinder": cbore_d_mm - 0.2, "axis": "z", "at": bolt_xy(),
"span": (-top + 0.1, top - cbore_depth_mm - 0.1)},
"min_mm3": 50.0},
# a user-stated hard limit, measured from the solid
{"feature": "clears the objective turret", "bbox_z": {"max": 15.0}},
]
```
Semantics:
| Key | Meaning |
| --- | --- |
| `clear` / `material` | region that must contain no material / must contain material |
| `{"cylinder": DIA, "axis": "x"\|"y"\|"z", "at": [(a, b), ...], "span": (lo, hi)}` | `at` is 2D in the plane perpendicular to the axis — axis `z`: (x, y); axis `x`: (y, z); axis `y`: (x, z). Omit `span` to run through the whole part |
| `{"box": (dx, dy, dz), "at": [(x, y, z), ...]}` | axis-aligned box gauges centred at each position |
| `tol_mm3` / `min_mm3` | pass thresholds per position (both default 0.01) |
| `bbox_x``bbox_z`, `bbox_min/mid/max` | `{"min": mm, "max": mm}` bounds on the measured bounding box |
Size the gauges from the same named constants as the geometry **only when the requirement is
relational** (the recess sits above the seat). When the requirement is absolute — a mating part's
MMC, a user's height limit, a beam position — write the gauge from the requirement's own numbers,
so a wrong parameter cannot shrink the gauge to match the wrong geometry.
For a one-off question without editing the model, `check.py probe` runs a single gauge from the
command line, and `check.py bores` prints a census of every cylindrical face (diameter, axis,
position, span, sweep) to reconcile against the model's intent.
## Positioning
`Locations` places the objects created inside it. It is the workhorse for bolt patterns.
```python
with Locations((10.0, 0.0), (-10.0, 0.0)): # two positions on the current plane
Hole(radius=3.3)
with Locations((0.0, 0.0, floor_t_mm)): # offset in z
Box(10.0, 10.0, 5.0, mode=Mode.SUBTRACT)
with GridLocations(9.0, 9.0, 12, 8): # x spacing, y spacing, x count, y count
Hole(radius=1.5)
```
`GridLocations` centres the grid on the origin. A microplate well grid is dimensioned from the
plate corner instead, so compute absolute positions and pass them to `Locations`:
```python
a1_x_mm, a1_y_mm, pitch_mm = 14.38, 11.24, 9.0 # slas-well-positions-96
origin_x = -plate_l_mm / 2
origin_y = plate_w_mm / 2
wells = [
(origin_x + a1_x_mm + pitch_mm * col, origin_y - a1_y_mm - pitch_mm * row)
for row in range(8) for col in range(12)
]
with Locations(*wells):
Hole(radius=well_clear_d_mm / 2)
```
## Alignment
By default objects are centred on the origin. `align` moves the datum, which is usually what you
want for a pocket that starts at a floor:
```python
Box(x, y, z, align=(Align.CENTER, Align.CENTER, Align.MIN)) # sits on z = 0
Box(x, y, z, align=(Align.MIN, Align.MIN, Align.MIN)) # corner at the origin
```
Getting this wrong is the classic "pocket cut through the floor" bug, and it is exactly what the
snapshot catches.
## Holes
`Hole` cuts through the whole part; `CounterBoreHole` and `CounterSinkHole` add a head recess.
**`CounterBoreHole` cuts downward from the workplane it is placed on, with the recess at that
plane.** On a centred `Box` the default workplane is the mid-height of the part, so a 2-tuple
location buries the screw seat inside the plate — or, on a thin plate, lets the recess swallow the
top entirely, leaving a straight bore the screw head falls through. Place it on the **top face**
(or give the location an explicit z at the top):
```python
with BuildPart() as plate:
Box(60.0, 60.0, 10.0) # spans z = -5 .. +5
top = plate.faces().sort_by(Axis.Z)[-1]
with Locations(top):
with Locations((20.0, 20.0)):
CounterBoreHole(radius=6.6 / 2, counter_bore_radius=11.0 / 2,
counter_bore_depth=6.5)
```
Size `counter_bore_depth` from the **screw head height**, not from habit: an M6 socket head cap
screw head is 6.0 mm tall, a 1/4-20 head 6.35 mm (`screw_head_height` in the breadboard
standards). A 4 mm counterbore leaves either head 2 mm proud — do not call that flush. After
generating, confirm in the snapshot (or a section) that the recess is at the top face and the
seat ledge exists; both failure modes here pass `is_valid` and the bounding box untouched.
Remember that printed holes come out undersize — see `references/fabrication-limits.md`.
## Selectors
Selectors find edges and faces to fillet, chamfer, or build on. The three you need:
```python
part.edges().filter_by(Axis.Z) # keep edges parallel to Z (the vertical corners)
part.edges().group_by(Axis.Z)[-1] # the group with the highest Z (the top edges)
part.faces().sort_by(Axis.Z)[-1] # the single highest face
part.edges().filter_by(GeomType.CIRCLE) # only circular edges
```
`filter_by` keeps everything matching. `group_by` partitions into lists ordered by the key, so
`[-1]` is the last group and `[0]` the first. `sort_by` orders individual items.
```python
with BuildPart() as ex:
Box(80.0, 60.0, 10.0)
chamfer(ex.edges().group_by(Axis.Z)[-1], length=4.0) # chamfer the top face edges
fillet(ex.edges().filter_by(Axis.Z), radius=5.0) # round the vertical corners
```
**These broad selectors are only safe on a part that is still a plain box.** Once the part has
pockets, bores, notches, or micro-relief, `filter_by(Axis.Z)` and `group_by(Axis.Z)[-1]` also
select the edges of those features, and the fillet either throws a kernel error
(`Failed creating a fillet`, `BRep_API: command not done`) or — worse — succeeds and silently eats
a wall or a 0.3 mm ridge. Both happen in practice. So:
- Fillet or chamfer the **outer body before adding internal features**, or filter the selection
down deliberately (by position, length, or `GeomType`) so only the intended edges remain.
- Bound the radius with `part.max_fillet(edges)` when the nearby geometry is tight — it returns
the largest radius the kernel can actually build on that edge set.
- Make every fillet/chamfer radius a named parameter, and on a kernel failure back the value off
rather than fighting the selector.
- Then check the snapshot: a consumed feature is obvious in the picture and invisible in
`is_valid`.
## Sketch then extrude
For a profile that is not a primitive, sketch it and extrude:
```python
with BuildPart() as bracket:
with BuildSketch() as profile:
Rectangle(40.0, 20.0)
with Locations((15.0, 0.0)):
Circle(radius=4.0, mode=Mode.SUBTRACT)
extrude(amount=6.0)
```
This is also the route to a laser-cut DXF: the sketch is the cut profile.
## Exports
`gen.py` handles these, but for reference:
```python
export_step(part, "part.step", unit=Unit.MM) # authoritative
export_stl(part, "part.stl", tolerance=1e-3, angular_tolerance=0.1)
# 2D profile for laser cutting. section() is a module-level operation, NOT a
# method on the shape -- part.section(...) raises AttributeError.
from build123d.exporters import ColorIndex # NOT exported by `from build123d import *`
profile = section(part, Plane.XY.offset(z_mm), mode=Mode.PRIVATE)
profile = profile.moved(Location((0, 0, -z_mm))) # back to z = 0, or the DXF writer
# warns about a non-planar shape
exporter = ExportDXF(unit=Unit.MM)
exporter.add_layer("CUT", color=ColorIndex.RED) # laser shops key power/speed to layers
exporter.add_shape(profile, layer="CUT")
exporter.write("part.dxf")
```
Cut the section through material, not at `z = 0`: a part modelled sitting on the build plate has
only a degenerate face there. `gen.py --dxf` defaults to the part's mid-height and takes `--dxf-z`
to override.
STEP preserves exact BREP geometry; STL is a triangulated approximation. **Always keep STEP as the
source of truth** and regenerate meshes from it, never the reverse.
## Measuring in code
Useful for asserting an interface inside the model itself:
```python
bbox = part.bounding_box()
print(bbox.size.X, bbox.size.Y, bbox.size.Z)
print(part.volume, part.area)
print(part.is_valid) # a property in 0.11.1, not a method
print(part.center(CenterOf.MASS))
```
`is_valid` being a property rather than a method is a real difference from older releases and from
some documentation. Access it without parentheses.
## Things that bite
- **`is_valid` is a property.** `part.is_valid()` raises `TypeError: 'bool' object is not callable`.
- **`section()` is a module-level operation, not a method.** `part.section(Plane.XY)` raises
`AttributeError`. Call `section(part, plane, mode=Mode.PRIVATE)`.
- **`intersect()` returns a `ShapeList`** with no `.volume`; the `&` operator returns a `Solid` that
has one. `check.py clearance` handles both.
- **Never name a script `inspect.py`** in a directory that lands on `sys.path`. It shadows the
standard library `inspect` module, which breaks `typing_extensions` and therefore build123d
itself. This is why the bundled script is `check.py`.
- **Builder objects are not parts.** Return `builder.part`, not the builder.
- **`Mode.SUBTRACT` needs an existing body.** Subtracting from an empty context does nothing
silently.
- **A swept or extruded profile is centred on its path/plane unless you align it.** Sweeping a
`Rectangle(w, h)` along a path on a surface leaves half the profile below the surface — a
"0.3 mm ridge" that is really 0.15 mm proud. Pass `align=` (and an explicit `x_dir` on the
profile plane) so the profile sits where you think it does, then measure the result.
- **`Curve` has no `.length`.** Sum the edges instead: `sum(e.length for e in curve.edges())`.
- **The boolean of touching or disjoint solids is empty, not an error.** Depending on the path you
get `None`, an empty `Compound`, or a `ShapeList` with no `.volume` — guard before reading
`.volume` in any interference check.
- **`ColorIndex` and `LineType` live in `build123d.exporters`**, not in the top-level namespace;
`from build123d import *` does not bring them in, and `add_layer(color=1)` fails.
- The OpenCascade kernel raises assorted exception types. Catch broadly around boolean operations
and report the failure rather than letting a traceback escape.
## Sources
- build123d documentation — <https://build123d.readthedocs.io/en/latest/>
- Introductory examples (builder vs algebra, selectors, fillets) —
<https://build123d.readthedocs.io/en/latest/introductory_examples.html>
- Import/export reference — <https://build123d.readthedocs.io/en/latest/import_export.html>
@@ -0,0 +1,169 @@
---
title: "Fabrication limits, tolerances, and materials"
task: ""
lineage_type: import
upstream_source: https://github.com/K-Dense-AI/scientific-agent-skills/blob/336c4f83/skills/lab-hardware-cad/references/fabrication-limits.md
upstream_sha: 336c4f83
imported_at: 2026-08-16
prompt_class: unknown
upstream_changes: accepted
author: upstream
validated: false
---
# Fabrication limits, tolerances, and materials
Read this before finalising any geometry. Process determines what geometry is possible; material
determines whether the part survives the lab.
## Process tolerances
Achievable tolerance and minimum feature size, as planning figures. **Every number here depends on
the specific machine, material, and operator.** Use them to choose a process and to size a first
article, then verify with a test coupon.
| Process | Typical tolerance | Min wall | Min feature | Notes |
| --- | --- | --- | --- | --- |
| FDM | ±0.3 mm (often worse over 100 mm) | 1.2 mm (3 x 0.4 mm nozzle) | ~0.8 mm | Anisotropic: much weaker across layers. Porous. |
| SLA / DLP | ±0.1 mm | 0.8 mm | ~0.3 mm | Better surface and detail. Resin choice dominates properties. |
| SLS (nylon) | ±0.2 mm | 0.8 mm | ~0.5 mm | Isotropic, no supports, slightly porous surface. |
| CNC milling | ±0.05 mm or better | 0.8 mm in metal | Set by tool diameter | Internal corners carry the tool radius — you cannot mill a sharp internal corner. |
| Laser cutting | ±0.1 mm | n/a | Kerf ~0.1-0.3 mm | 2D only. Edge taper on thick stock. Kerf offset must be applied. |
Two consequences that catch people:
- **Holes print undersize** on both FDM and SLA. A 6.0 mm modelled hole typically measures under
6.0 mm. Oversize functional bores, or plan to ream them.
- **Internal corners cannot be sharp in milling.** If a milled pocket must accept a square part,
add corner relief cuts. (For a part with *rounded* corners the tool radius is harmless as long
as it stays at or below the part's minimum corner radius — see the corner-radius rule in
`references/labware-adapters.md`.)
### Laser cutting
- **Kerf direction is fixed by the physics, so get it right in the handover.** The beam removes a
strip of width k (~0.10.3 mm) centred on the drawn line. Cutting on the line therefore makes
**holes and internal cutouts come out oversize by ~k, and the part's outer outline undersize by
~k**. Say which convention the DXF uses (on-the-line is the default assumption) and let the shop
offset, or offset the geometry yourself and say so — never both.
- **Put cut geometry on a named layer** (one layer per operation: `CUT`, `ENGRAVE`). Shops key
power and speed to layer or colour; geometry on layer 0 forces them to guess.
- **Cut order matters:** internal features before the outer outline, or the part shifts once it is
freed from the sheet.
- **Sheet stock is not its nominal thickness.** "3 mm" acrylic commonly runs ~2.83.2 mm; slots
sized for nominal will be loose or tight. For solvent-welded joints prefer **cast** acrylic over
extruded — cleaner cut edge, less vapour crazing — and remember alcohols craze acrylic either
way (see Chemical, below).
- Laser-cut edges are sharp and slightly tapered; call out deburring or flame-polishing for
anything handled or animal-facing.
## Fits and clearances
Nominal dimensions do not produce fits. Choose a clearance deliberately, per side:
| Fit | FDM | SLA | CNC |
| --- | --- | --- | --- |
| Free-sliding (a plate dropping into a pocket) | 0.40 mm | 0.20 mm | 0.10 mm |
| Located but removable by hand | 0.25 mm | 0.10 mm | 0.05 mm |
| Press / interference | -0.05 mm | -0.03 mm | -0.02 mm |
Then remember the **other** part has tolerance too. When mating to a standardised component,
design the receiving feature against the component's **maximum material condition**, not its
nominal — a pocket sized from nominal fits only the smaller half of conforming parts. This is what
`intent: "envelope"` enforces. Declare it in the model and check the manifest:
```bash
python scripts/check.py interfaces out/part.manifest.json
```
Or check a single number by hand:
```bash
python scripts/check.py fit --standard slas-microplate-footprint \
--intent envelope --clearance 0.8 --value footprint_length=128.81
```
## Threads and inserts
**Printed threads are usually a mistake.** Layer resolution is comparable to the thread pitch, so
printed threads are weak, dimensionally unreliable, and shed particles.
In descending order of preference:
1. **Heat-set threaded inserts** — the standard solution for printed parts. Model a straight bore
to the insert manufacturer's specified diameter (it varies by insert; get the datasheet) and
provide enough surrounding wall, typically at least 2 mm.
2. **Clearance hole plus a captive nut** in a hex pocket. Reliable and cheap.
3. **Tapping the printed material directly** — acceptable for light, infrequently-assembled joints.
4. **Printing the thread** — only for coarse threads (roughly M6 and above), never for fine
threads like the 0.635 mm pitch SM1 (see `references/optomechanics.md`).
## Orientation and anisotropy
For FDM especially, orientation is a design decision, not a printing detail:
- Parts are substantially weaker **across** layers than along them. Orient so that load runs
along layers, and state the intended orientation in the model docstring.
- Overhangs beyond roughly 45 degrees need support, and supported surfaces come out rough and
dimensionally poor. If a surface is a sealing or mating face, orient it so it is not supported.
- Holes printed with their axis vertical are round; printed horizontally they come out with a
drooped top. Teardrop or chamfer horizontal holes that must stay round.
- **Every enclosed cavity needs a drain path** in resin printing. See
`references/microfluidics.md`.
## Materials
### Thermal
| Material | Approximate service limit | Autoclave (121 °C)? |
| --- | --- | --- |
| PLA | ~50-60 °C | **No** — distorts well below autoclave temperature |
| PETG | ~70-80 °C | No |
| ABS / ASA | ~90-100 °C | Marginal, generally no |
| Polypropylene | ~100 °C | Marginal |
| Nylon (SLS) | ~120-160 °C | Sometimes; verify per grade |
| PEEK | >250 °C | Yes |
| Stainless steel, aluminium, glass | High | Yes |
**Assume a printed part is not autoclavable unless it is a verified high-temperature material.**
Offer chemical or gas sterilisation as the alternative, and check that against the solvent notes
below.
### Chemical
- **Acrylic (PMMA)** crazes on contact with alcohols, including 70% ethanol — a serious problem in
a lab that disinfects everything with ethanol.
- **Polycarbonate** is attacked by many solvents and by some alkaline cleaners.
- **PLA** hydrolyses; it degrades in warm, wet, or repeatedly-cleaned service.
- **PP, PTFE, PEEK** have broad chemical resistance and are the safe choices for solvent contact.
Always ask what the part will be cleaned with, not just what it will contain. Cleaning agent
compatibility is more often the failure than the sample.
### Biocompatibility
- **Uncured SLA resin is cytotoxic.** Even nominally biocompatible resins require the
manufacturer's full post-cure and wash protocol, and leachables can still affect sensitive cell
assays.
- For anything contacting cells, tissue, or animals: prefer glass, medical-grade polymer, or PTFE
for the contact surface, and use the printed part as a holder that does not touch the sample.
- "Biocompatible" on a resin datasheet refers to a specific certified process and application. It
does not transfer to your printer, your cure schedule, or your assay. Say this rather than
implying a printed part is cell-safe.
### Optical
- Printed and milled surfaces scatter; they are not optical surfaces.
- Most printed resins **autofluoresce**, often strongly, which contaminates fluorescence readouts.
- Black is not automatically non-reflective.
- Where an optical surface is needed, use glass or a bonded film and model the holder around it.
## Cost and lead-time reality
Mention these when recommending a process: FDM is hours and pennies; SLA is hours and modest cost;
SLS and CNC are typically outsourced with days of lead time and much higher cost. A design that
needs ±0.05 mm has committed the user to CNC — flag that trade before they discover it at quoting.
## Before fabrication
Work through `references/validation.md`.
@@ -0,0 +1,203 @@
---
title: "Labware adapters, holders, and racks"
task: ""
lineage_type: import
upstream_source: https://github.com/K-Dense-AI/scientific-agent-skills/blob/336c4f83/skills/lab-hardware-cad/references/labware-adapters.md
upstream_sha: 336c4f83
imported_at: 2026-08-16
prompt_class: unknown
upstream_changes: accepted
author: upstream
validated: false
---
# Labware adapters, holders, and racks
Parts that receive standard consumables: microplates, cuvettes, tubes, slides, dishes.
The governing principle: **where a published standard exists, design to the standard; where it
does not, require a measurement.** Microplate footprints are standardised. Well geometry, skirt
profiles, tube dimensions, and lid fits are not.
Verified dimensions live in `assets/standards.json`. Query them rather than copying numbers:
```bash
python scripts/check.py standards --show slas-microplate-footprint
```
## Microplates (ANSI/SLAS 1-4)
Four documents split the plate geometry. All are ANSI-approved and were reaffirmed in 2012.
| Document | Governs | Key numbers |
| --- | --- | --- |
| ANSI/SLAS 1-2004 | Footprint | 127.76 x 85.48 mm ±0.25; corner radius 3.18 ±1.6 mm |
| ANSI/SLAS 2-2004 | Height | 14.35 ±0.25 mm, resting plane to top of perimeter wells |
| ANSI/SLAS 3-2004 | Bottom outside flange | Short 2.41, medium 6.10, tall 7.62 mm, each ±0.38 |
| ANSI/SLAS 4-2004 | Well positions | 96-well: 9.0 mm pitch, A1 at 14.38 mm from left, 11.24 mm from top |
### Designing a plate pocket
Three traps, in the order people fall into them.
**1. Design to maximum material, not to nominal.** A plate at the top of tolerance is
127.76 + 0.25 = 128.01 mm. A pocket cut at 127.76 + clearance will jam on roughly half the plates
you try. Compute:
```python
plate_l_mm = 127.76 # ANSI/SLAS 1-2004 nominal
plate_tol_mm = 0.25 # ANSI/SLAS 1-2004
fit_clearance_mm = 0.40 # per side; FDM, see fabrication-limits.md
pocket_l_mm = plate_l_mm + plate_tol_mm + 2 * fit_clearance_mm # 128.81
```
**2. The corner radius tolerance is enormous — and it bounds the pocket radius from above,
not below.** 3.18 ±1.6 mm means a real plate corner is anywhere from 1.58 to 4.78 mm. Get the
direction right: a plate corner is **convex**, a pocket fillet is **concave material bulging
inward**, so a *sharp* internal pocket corner always clears a rounded plate — the unused corner is
empty space. It is a pocket fillet *larger* than the plate's corner radius that binds: the bulge
occupies space the plate needs. Sizing the fillet to the plate's maximum corner radius is
therefore exactly backwards — it binds every plate except those at the top of the corner
tolerance.
The safe options, best first:
- **Corner relief** (a small slot or bore cut past each corner) — always clears, prints and mills
cleanly, and is the standard fix.
- **Fillet no larger than the plate's minimum corner radius** (1.58 mm for SLAS plates) — clears
every conforming plate in every position.
- A larger fillet only if `R ≤ r_min + ~3.4 × per-side clearance` — the geometry only recovers the
intrusion when the plate stays roughly centred, so treat this as a last resort and say so.
```python
with BuildPart() as pocket:
# ... pocket geometry ...
# relief bores just outside each pocket corner: clears any conforming corner radius
with Locations(*corner_relief_centres()):
Hole(radius=2.0)
```
**3. Height depends on the flange, not just the plate.** ANSI/SLAS 3 standardises three flange
heights. A carrier that grips the flange must be told which one. Ask; do not assume medium.
### Well grid
For a part that must reach individual wells — a magnet block, a lid with access holes, a light
guide — lay out from the plate's outline corner, not from the plate centre:
```python
a1_x_mm, a1_y_mm, pitch_mm = 14.38, 11.24, 9.0 # ANSI/SLAS 4-2004, 96-well
locations = [
(a1_x_mm + pitch_mm * col, a1_y_mm + pitch_mm * row)
for row in range(8) for col in range(12)
]
```
The standard's positional tolerance is a **0.70 mm diameter zone** around each nominal centre, not
a ±0.70 mm band. A feature that must clear every well needs at least 0.35 mm of radial margin on
top of your own process tolerance.
384-well pitch is 4.5 mm and 1536-well pitch is 2.25 mm. **The A1 offsets for those formats in
`standards.json` are marked unverified** — they were derived, not read from the document. Read
ANSI/SLAS 4-2004 before relying on them.
### What the standards do not fix
Well diameter, well depth, well bottom shape (flat, round, conical), skirt height, lid geometry,
optical bottom thickness, and deep-well plate height. All vary by manufacturer and product line.
If the part touches any of these, get the vendor drawing or measure it.
## Cuvettes
The standard macro cuvette is a convention rather than a published standard, but it is close to
universal: **12.5 x 12.5 mm external, 45 mm tall, 1.25 mm wall, 10 mm optical path**.
Design notes:
- Holders should be generous or compliant. Because no document fixes the tolerance, a 0.1 mm
interference fit designed against nominal will fail on some suppliers' cuvettes.
- Semi-micro and micro cuvettes keep the 12.5 mm external footprint but change internal geometry
and often height. A holder designed for the external footprint accommodates all of them; one
designed around the sample volume does not.
- Cuvettes are usually held with a spring or leaf on one face so the two optical faces register
against fixed datums. Copy that: locate on two adjacent faces, preload from the opposite corner.
A four-sided pocket with clearance lets the cuvette rotate and shifts the path length.
- **Never print the optical path.** Printed surfaces scatter. The cuvette provides the optical
faces; the holder provides position only, and must not obstruct the beam window.
## Tubes
Tube dimensions are **not standardised** and differ measurably between suppliers, and often
between product lines from the same supplier. Approximate outside diameters near the tube rim:
| Tube | Approximate OD | Note |
| --- | --- | --- |
| 0.2 mL PCR | 6 mm | Often supplied in strips or as a 96-format plate |
| 1.5 mL microcentrifuge | 11 mm | Rim is wider than the body; the body tapers |
| 2.0 mL microcentrifuge | 11 mm | Same rim as 1.5 mL, taller body |
| 15 mL conical | 17 mm | Cap is wider than the tube |
| 50 mL conical | 30 mm | Cap is wider than the tube |
**Treat every number in this table as a starting point for a first article, not a design input.**
Ask the user for the supplier and catalogue number, or ask them to measure with calipers. Then
design a rack that holds the tube by the **rim or the cap**, which is dimensionally stable, rather
than by the tapered body, which is not.
For a rack, the useful pattern is a through-hole sized to the body plus clearance and a counterbore
that catches the rim, so the tube hangs rather than bottoms out.
## Microscope slides and coverslips
Standard slide: **75 x 25 mm, 1.0 mm thick** (ISO 8037-1 covers slide dimensions; thickness classes
vary, and 1.0-1.2 mm is typical). Coverslips are specified by thickness number, not dimension:
#1 is roughly 0.13-0.17 mm and #1.5 roughly 0.16-0.19 mm.
Objective working distance is unforgiving. A holder that adds even 0.2 mm under the slide can put
the sample outside a high-NA objective's working distance. Design slide holders so the slide
registers directly against the stage datum, with the holder clamping from above.
## Petri dishes and stage inserts
Standard dish outside diameters are approximately 35, 60, 90, and 100 mm, but the flange profile
and lid fit vary. Dishes are also slightly out of round. Locate on three points rather than a
continuous circular pocket: a three-point nest is insensitive to ovality, a close-fitting bore is
not.
For stage inserts, the interface that matters is the **microscope stage opening**, which is
instrument-specific and must be measured. Many stages accept a standard SLAS-footprint insert;
confirm before assuming it.
## Checks to run
Declare the pocket in the model's `interfaces()` and let the check read it:
```bash
python scripts/gen.py carrier_model.py --outdir out/
python scripts/check.py interfaces out/carrier.manifest.json
```
**Do not point `check.py fit` at the carrier's STEP.** `fit` measures the outer bounding box, which
for a carrier is the outside of its walls — 6 mm larger than the pocket here — so it fails against
the plate footprint no matter how correct the pocket is. The dimension that matters is internal, so
it has to be declared, not measured from the envelope.
To check the number by hand instead:
```bash
python scripts/check.py fit --standard slas-microplate-footprint \
--intent envelope --clearance 0.8 --value footprint_length=128.81
```
`--intent envelope` checks one-sided against maximum material condition, and `--clearance` is the
total intended clearance: 0.40 mm per side is 0.80 mm. Passing means the pocket is the size you
intended, not that the plate fits — only a test print shows that.
Then always run `snapshot.py` and confirm the pocket is on the face you meant.
## Sources
- ANSI/SLAS 1-2004 (R2012) Footprint Dimensions — <https://www.slas.org/SLAS/assets/File/public/standards/ANSI_SLAS_1-2004_FootprintDimensions.pdf>
- ANSI/SLAS 2-2004 (R2012) Height Dimensions — <https://www.slas.org/SLAS/assets/File/public/standards/ANSI_SLAS_2-2004_HeightDimensions.pdf>
- ANSI/SLAS 3-2004 (R2012) Bottom Outside Flange Dimensions — <https://www.slas.org/SLAS/assets/File/public/standards/ANSI_SLAS_3-2004_BottomOutsideFlangeDimensions.pdf>
- ANSI/SLAS 4-2004 (R2012) Well Positions — <https://www.slas.org/SLAS/assets/File/public/standards/ANSI_SLAS_4-2004_WellPositions.pdf>
- SLAS microplate standards overview — <https://www.slas.org/education/ansi-slas-microplate-standards/>
@@ -0,0 +1,170 @@
---
title: "Microfluidic chips, molds, and flow cells"
task: ""
lineage_type: import
upstream_source: https://github.com/K-Dense-AI/scientific-agent-skills/blob/336c4f83/skills/lab-hardware-cad/references/microfluidics.md
upstream_sha: 336c4f83
imported_at: 2026-08-16
prompt_class: unknown
upstream_changes: accepted
author: upstream
validated: false
---
# Microfluidic chips, molds, and flow cells
Channel networks, soft-lithography molds, printed chips, gaskets, and manifolds.
## First: decide what you are actually modelling
This is the error that wastes the most time in microfluidic CAD. Three different objects get
called "the chip":
| Object | Channels are | Made by |
| --- | --- | --- |
| **Mold / master** | **Raised ridges** (positive relief) | Photolithography on a wafer, SLA print, or micromilling |
| **Cast chip** | **Recessed grooves** (negative of the mold) | PDMS cast against the mold, then bonded to a substrate |
| **Directly-fabricated chip** | **Recessed grooves or enclosed lumens** | Printed, milled, or laser-cut directly |
A model that is correct as a chip is exactly wrong as a mold. Put the polarity in the module
docstring and in a named parameter, and **verify it numerically, not by eye**: inverted polarity
is invisible in the bounding box, the volume, and the validity check — and at typical channel
scale (a 0.3 mm ridge on a 40+ mm part) it is invisible in an outline render too, because raised
and recessed features draw the same edges. Declare it as geometry checks instead
(`references/build123d-patterns.md`): a `material` region where the ridge must stand above the
casting surface, and a `clear` region over the rest of that layer — a groove fails the first,
an inverted full-area layer fails the second. For a one-off question,
`check.py probe <step> --box ... --expect material` answers it without editing the model. State
the measured relief height in the report. Use the snapshot for layout and connectivity, which
it does show well.
```python
polarity = "mold" # "mold" = raised ridges; "chip" = recessed grooves
```
If casting PDMS, the mold also needs a **surrounding wall or a casting frame** to contain the
uncured polymer, and enough flat land around the features for the cast part to release.
## Channel cross-section and aspect ratio
Channels are usually rectangular because that is what planar fabrication produces. Two failure
modes bound the aspect ratio, and both are geometric:
- **Roof sag / collapse** — a channel much wider than it is tall has an unsupported ceiling. In
PDMS the roof bows down and can stick to the floor. Commonly cited guidance keeps
**width : height below roughly 10 : 1**; wide channels need support pillars.
- **Sidewall collapse** — a mold ridge much taller than it is wide falls over or fails to release.
Keep **height : width below roughly 10 : 1** on the mold.
Treat both as rules of thumb, not guarantees: the real limits depend on PDMS mixing ratio, cure
schedule, and applied pressure. For anything load-bearing or high-pressure, prototype.
Also keep **channel-to-channel spacing at least the channel height**, so the wall between two
channels does not deflect or leak, and leave a flat **bonding land** — typically 1 mm or more of
uninterrupted flat surface around the network perimeter — for plasma or adhesive bonding.
## Minimum features by process
Achievable feature size drives the entire design, and the range across processes is three orders
of magnitude. Confirm against your specific tool before committing.
| Process | Practical minimum channel | Notes |
| --- | --- | --- |
| SU-8 photolithography | ~1-10 µm wide, 1-200+ µm tall | The reference process for soft lithography. Feature height is set by spin speed and resist grade. |
| Two-photon / µSLA | ~10-50 µm | Small build volume, slow, expensive. |
| Desktop SLA / DLP | ~200-500 µm | Uncured resin is very hard to clear from smaller lumens. Enclosed channels below ~0.5 mm frequently print blocked. |
| Micromilling | ~100 µm | Set by end-mill diameter; depth limited by tool aspect ratio. Leaves tool marks that scatter light. |
| FDM | Not suitable for sealed channels | Layer porosity leaks. Use only for holders and manifolds. |
| Laser-cut film / gasket | ~200 µm | Excellent for stacked-layer devices and gaskets. |
**Design enclosed printed channels for drainage.** Every lumen needs a path for uncured resin to
escape, and orientation on the build plate determines whether it drains. If the user is printing,
say which way up.
## Ports and tubing
The port is where most chips leak. Options, roughly in order of how common they are in a research
lab:
- **Direct tubing insertion** — a bore slightly *under* the tubing OD so the tubing seals by
interference. For 1/16 inch OD tubing (1.5875 mm), a bore around 1.5 mm in PDMS is typical. This
works in elastomer and fails in rigid printed parts, which crack instead of gripping.
- **Luer taper** — the standard syringe interface, a **6% taper** (ISO 80369-7 supersedes the
legacy ISO 594 series for medical use). Convenient, low pressure only. If you model a Luer taper,
get the profile from the standard, not from memory.
- **Threaded fittings** — flat-bottom **1/4-28 UNF** is the common lab standard for low-pressure
fluidics; **10-32 coned** is used at higher pressures. These need a tapped or heat-set-insert
port and a matching flat sealing face.
- **Barbs** — reliable with soft tubing and a clamp, bulky.
Whichever you choose, the sealing surface must be **flat and normal to the port axis**. A port
face left at a printed layer angle will not seal.
## Dead volume
Dead volume dominates the response time of any perfusion or gradient device, and it is trivially
computable, so compute it rather than estimating:
```
V = pi * r^2 * L # round tubing / bore
V = w * h * L # rectangular channel
```
Report the volume of every connecting bore alongside the channel network volume. A 20 mm long
1 mm bore holds ~15.7 µL, which is often larger than the entire channel network it feeds.
## Flow regime sanity check
Microfluidic flow is almost always laminar, but state it rather than assuming:
```
Re = rho * v * D_h / mu
D_h = 2 * w * h / (w + h) # hydraulic diameter, rectangular channel
```
For water in a 100 µm channel at 1 mm/s, Re is of order 0.1 — deeply laminar, so mixing is
diffusive only. If the design depends on mixing, it needs a mixer geometry (serpentine,
herringbone, or split-and-recombine); relying on turbulence will not work at these scales.
Pressure drop for a rectangular channel scales steeply with the smaller dimension. **Halving
channel height raises pressure drop by roughly an order of magnitude.** Check that the intended
pump or syringe can actually deliver it before finalising the cross-section.
## Material and optical constraints
- **PDMS** absorbs small hydrophobic molecules and is gas-permeable. Both are sometimes features
(oxygenation in organ-on-chip) and sometimes fatal to an assay (drug studies).
- **SLA resins** are frequently cytotoxic uncured and often still after a nominal cure. For cell
work, require post-cure plus a documented biocompatibility check, or use a different process.
See `references/fabrication-limits.md`.
- **Autofluorescence** matters for any fluorescence readout. Most printed resins autofluoresce
strongly. Image through glass or a thin COC/COP film, not through printed material.
- **Optical path**: printed and milled surfaces scatter. Any imaging window should be a bonded
coverslip or film, and the model must specify its thickness so the objective working distance
works out.
## Checks to run
```bash
python scripts/gen.py chip_model.py --outdir out/
python scripts/check.py facts out/chip.step
python scripts/snapshot.py out/chip.step --out out/chip.png
```
`facts` gives the volume; compare it against your hand-computed channel volume as an independent
check that the network is actually open and connected. A network modelled as a solid rather than a
cavity shows up immediately as a volume far larger than expected.
Then read the snapshot and confirm, explicitly:
1. **Polarity** — ridges for a mold, grooves for a chip.
2. Every port lands on the channel it should, and passes fully through to the surface.
3. The bonding land is continuous around the network.
4. No channel has been closed off or consumed by a fillet.
## Sources
- ISO 80369-7 (Luer connectors for intravascular applications) supersedes the ISO 594 series.
Obtain the taper profile from the standard itself.
- Aspect-ratio and spacing guidance here is standard soft-lithography practice; the numerical
limits are rules of thumb and depend on material and process. Prototype before committing.
@@ -0,0 +1,161 @@
---
title: "Optomechanical mounts and breadboard hardware"
task: ""
lineage_type: import
upstream_source: https://github.com/K-Dense-AI/scientific-agent-skills/blob/336c4f83/skills/lab-hardware-cad/references/optomechanics.md
upstream_sha: 336c4f83
imported_at: 2026-08-16
prompt_class: unknown
upstream_changes: accepted
author: upstream
validated: false
---
# Optomechanical mounts and breadboard hardware
Parts that bolt to an optical table, join a cage system, hold an optic or a sample in a beam path,
or carry a camera or objective.
Verified dimensions are in `assets/standards.json`:
```bash
python scripts/check.py standards --show optical-breadboard-metric
python scripts/check.py standards --show cage-system-30mm
python scripts/check.py standards --show sm1-lens-tube-thread
```
## Ask which system before you model anything
**Metric and imperial optical hardware are not interchangeable, and the difference is small enough
to look like a rounding error and large enough to prevent assembly.**
| | Metric | Imperial |
| --- | --- | --- |
| Grid pitch | 25.0 mm | 25.4 mm (1 inch) |
| Tapped hole | M6 x 1.0 | 1/4-20 UNC |
| Typical border | 12.5 mm | 12.7 mm |
Over a four-hole span the grids differ by **1.6 mm** — far more than any clearance hole absorbs.
There is no way to infer which the user has from the request. Ask. If the answer is unavailable,
model the mounting features as **slots along the bolt line** rather than round holes, which
tolerates both, and say that is what you did and why.
## Mounting to the table
- Use **clearance holes, not tapped holes**, in the part. The table is tapped; the part is
clearanced. For M6 use 6.6 mm (normal fit) in a printed part rather than 6.4 mm — printed holes
come out undersize.
- **Counterbore for the screw head** if the part surface must stay clear: roughly 11 mm diameter
for an M6 socket head cap screw, 11.2 mm for 1/4-20.
- **Never rely on more than two holes to locate a part.** Grid tolerance plus print tolerance means
a rigid four-hole pattern will bind. Round hole + slot is the standard fix: one hole locates, the
slot takes up the error.
- Printed parts are compliant. For anything where pointing stability matters, a printed mount is a
prototyping aid, not a final part — thermal drift and creep in polymer are large compared with
optical alignment tolerances. Say so when recommending one.
## Posts and pedestals
Common conventions, which vary by vendor — **confirm against the catalogue before use**:
- Imperial posts are Ø1/2 inch (12.7 mm), typically tapped 8-32 at one end with a 1/4-20 stud or
clearance at the other.
- Metric posts are Ø12 mm, typically tapped M4 with an M6 interface to the table.
- A post-holder plus post is height-adjustable but adds a compliant joint; a pedestal or a
solid machined riser is stiffer.
**Beam height** is a project-wide constant, not a per-part choice. Every mount on the table must
put its optic at the same height. Common conventions are 3 inches (76.2 mm) or 100 mm, but this is
a lab-by-lab choice. Ask for the number, define it once as `beam_height_mm`, and derive every
mount's optic centre from it.
## 30 mm cage system
The dominant convention for small free-space assemblies:
- **Rod spacing 30.0 mm** on a square, centred on the optical axis.
- **Rods Ø6 mm** (ER series).
- Standard cage plates are 0.35 inch (8.9 mm) thick.
For a custom cage plate: place four bores on a 30 mm square, put the aperture at the **centroid**
of those four bores, and bore them for a free-sliding fit **at your process's clearance**
(fabrication-limits.md): about 6.2 mm CNC, 6.4 mm SLA, 6.8 mm FDM. 6.1 mm is a reamed-metal
number — printed bores come out undersize, and four bores on a common square over-constrain each
other, so tighter is not better here. A cage plate that binds on the rods is worse than useless
because it transmits stress into the whole assembly.
Cage plates stack along the rods, so a custom plate's thickness directly consumes optical path
length. Budget it.
## Lens tube threads (SM series)
**SM1 is a 1.035 inch-40 thread**, which holds Ø1 inch (25.4 mm) optics. That is a **0.635 mm
pitch**.
**Do not print SM threads.** A 0.635 mm pitch is at or below the practical resolution of FDM and
marginal on desktop SLA; a printed SM1 thread will either not engage or will gall and shed
particles into the beam path. Instead:
- bore a clearance hole and use a purchased SM1 adapter or retaining ring, or
- design for a threaded metal insert, or
- clamp the optic directly with a retaining flange and screws.
If the design truly requires a printed thread, say explicitly that it needs test printing and is
likely to fail.
## Holding an optic
- **Never clamp an optic on its clear aperture.** Contact only the outer annulus of the face or the
edge. Define `clear_aperture_mm` as a named parameter and confirm in the snapshot that nothing
intrudes on it.
- Three-point contact is kinematically correct and does not deform the optic. A continuous
circular seat over-constrains it and induces stress birefringence, which matters for
polarisation work.
- Leave clearance for thermal expansion. A metal-in-polymer mount that is a press fit at 20 °C can
crack or bind across a temperature swing.
- Retaining forces should be light and distributed. A single set screw pressing on glass is a way
to chip glass.
## Stray light and scatter
Geometry is not the whole design here, and a STEP file cannot show any of this:
- Printed surfaces scatter strongly. Any surface that sees the beam should be baffled, angled away
from the optical axis, or treated.
- **Black does not mean non-reflective.** Black resin and black filament are often quite specular.
Specify a genuinely absorbing surface treatment where it matters.
- Thread and layer lines act as diffraction structures near a focus.
- For fluorescence work, printed material near the sample can autofluoresce into the detection
path.
Flag these to the user; do not silently assume a printed enclosure is light-tight.
## Checks to run
```bash
python scripts/gen.py mount_model.py --outdir out/
python scripts/check.py facts out/mount.step
python scripts/check.py interfaces out/mount.manifest.json
python scripts/snapshot.py out/mount.step --out out/mount.png
```
Declare the grid pitch, rod spacing, and bore diameters in the model's `interfaces()` against
`optical-breadboard-metric`, `optical-breadboard-imperial`, or `cage-system-30mm`, so the check
catches a 25.0-for-25.4 substitution rather than leaving it to a reader.
There is still **no automatic bolt-pattern check** — the interface check compares dimensions, not
hole positions. Compute the pattern in the model from a named `grid_pitch_mm` constant, and confirm
in the snapshot that:
1. All mounting holes are present and pass fully through.
2. The optic aperture is centred where you intended, and unobstructed.
3. Counterbores are on the accessible face.
4. Nothing intrudes into the clear aperture or the beam path.
## Sources
- Thorlabs imperial and metric threading — <https://www.thorlabs.com/imperial-and-metric-threading>
- Thorlabs standard 30 mm cage plates — <https://www.thorlabs.com/newgrouppage9.cfm?objectgroup_ID=2273>
- Thorlabs SM1 lens tube compatible cage plates — <https://www.thorlabs.com/newgrouppage9.cfm?objectgroup_id=4114>
- Post dimensions, beam heights, and vendor-specific thread conventions in this file are common
conventions rather than published standards. Confirm against the catalogue.
@@ -0,0 +1,145 @@
---
title: "Pre-fabrication validation checklist"
task: ""
lineage_type: import
upstream_source: https://github.com/K-Dense-AI/scientific-agent-skills/blob/336c4f83/skills/lab-hardware-cad/references/validation.md
upstream_sha: 336c4f83
imported_at: 2026-08-16
prompt_class: unknown
upstream_changes: accepted
author: upstream
validated: false
---
# Pre-fabrication validation checklist
Work through this before telling a user a part is ready to fabricate. Each item names the failure
it catches, because a checklist without consequences gets skipped.
## 1. Provenance
- [ ] The STEP was produced by `gen.py` from the current model source.
*Catches: a stale artifact that no longer matches the code you just edited.*
- [ ] A `*.manifest.json` exists alongside it, and its `source.sha256` matches the model file.
*Catches: silently editing an exported STEP, which makes the design unreproducible.*
- [ ] The manifest's `interfaces` block lists every dimension a bundled standard covers, and its
values are the ones the model computed after any `--param` override. Empty is correct only
when nothing on the part mates with a bundled standard — and then every interface dimension
is named as unchecked in the report instead.
*Catches: a static `INTERFACES` list frozen at import, recording pre-override numbers; and
an interface that silently escaped checking.*
- [ ] Every parameter in the model is named with units.
*Catches: the bare `12.7` nobody can later identify as half an inch.*
```bash
python scripts/gen.py part_model.py --outdir out/
```
## 2. Geometry is sound
- [ ] `is_valid` is true.
*Catches: self-intersecting or non-manifold solids that slicers and CAM silently mangle.*
- [ ] `solid_count` is what you expect — usually 1.
*Catches: a boolean that failed and left two disjoint lumps, or a feature floating free of
the body.*
- [ ] Volume is plausible for the part's size and wall thickness.
*Catches: a cavity modelled solid, or a subtract that did nothing.*
- [ ] Every geometric requirement in the request is declared in `checks()` and passes — clear
regions for what must pass through or fit in, material regions for what must remain,
bbox bounds for stated size limits.
*Catches: a recess that swallowed its screw seat, a pocket the mating part cannot enter,
a beam corridor with a wall in it, a feature a fillet silently ate — all invisible to
`is_valid` and the bounding box.*
```bash
python scripts/check.py facts out/part.step
python scripts/check.py geometry out/part.step --model part_model.py
```
## 3. Interfaces
- [ ] Every interface dimension has a written source: a standard ID, a vendor drawing, or a user
measurement. **None came from memory.**
*Catches: the single most expensive failure mode in this skill.*
- [ ] Every interface covered by a standard is declared in the model's `interfaces()` and passes
`check.py interfaces`.
*Catches: an interface nobody checked because the outer bounding box could not see it.*
- [ ] Features that receive a standardised component use `intent: "envelope"`.
*Catches: a pocket sized to nominal, which fits only the smaller half of conforming parts.*
- [ ] Any standard entry marked `verified: false` was confirmed against the primary document, or
the user was told it is unconfirmed.
*Catches: propagating a derived number as if it were read from the standard.*
- [ ] Metric vs imperial is confirmed where both exist, and no expression mixes them.
*Catches: the 25.0 vs 25.4 mm grid error, which accumulates to 1.6 mm over four holes.*
- [ ] Interfaces not covered by any bundled standard — a vendor drawing, a measurement — were
reported to the user as unchecked, with the number and its source.
*Catches: a silent gap where the automatic check simply had nothing to say.*
```bash
python scripts/check.py interfaces out/part.manifest.json
# one dimension by hand, when it is not declared in the model
python scripts/check.py fit --standard <id> --intent envelope --clearance <mm> --value <dim>=<mm>
```
## 4. Fits and assembly
- [ ] Every mating dimension has a deliberate clearance chosen for the process.
*Catches: nominal-to-nominal fits, which do not assemble.*
- [ ] Multi-part assemblies were checked for interference.
*Catches: parts that overlap in CAD and therefore cannot exist together.*
- [ ] Rigid multi-hole mounting patterns have at least one slot.
*Catches: a four-hole bolt pattern binding on accumulated tolerance.*
```bash
python scripts/check.py clearance out/a.step out/b.step --min 0.3
```
## 5. Manufacturability
- [ ] Minimum wall and feature sizes are within the chosen process (`fabrication-limits.md`).
- [ ] Print or machining orientation is stated, and load runs along layers, not across them.
- [ ] Threads use inserts or captive nuts rather than printed threads, unless coarse.
- [ ] Enclosed cavities have a drain path for resin, and support-free access where possible.
- [ ] Milled internal corners have relief for the tool radius.
## 6. Material
- [ ] Material is compatible with the **cleaning agent**, not only the sample.
*Catches: acrylic crazing on 70% ethanol; PLA distorting in an autoclave.*
- [ ] Sterilisation method is stated and the material actually survives it.
- [ ] Anything contacting cells, tissue, or animals has a justified material, or contact is
designed out.
*Catches: assuming a printed resin part is cell-safe.*
- [ ] Optical requirements — autofluorescence, scatter, transmission — are addressed if the part is
near a beam or a detector.
## 7. Visual review — mandatory
- [ ] A snapshot was rendered **and read** after the most recent generation.
- [ ] Confirmed in the image: features on the intended faces; correct mold/chip polarity; every
port, bore, and boss present, inside the body, and passing through; nothing consumed by a
fillet; clear apertures unobstructed.
```bash
python scripts/snapshot.py out/part.step --out out/part.png
```
**This step is never waived by the numeric checks passing.** `is_valid: true` with a correct
bounding box is fully consistent with a pocket cut on the wrong face or an inverted mold. Those
errors are obvious in the picture and invisible in the numbers.
## 8. Report
Give the user, explicitly:
1. Process and material, and why.
2. Every interface dimension with its source and tolerance.
3. Clearances chosen, and the fit class they came from.
4. What the snapshot showed — described, not merely "a snapshot was generated".
5. Every check that did not pass, and every dimension you could not verify.
6. A recommendation to print a test coupon of the critical interface before committing to the full
part, whenever the design depends on a fit.
State the unverified items plainly. A part list with one honest "this dimension needs
confirmation" is far more useful than a confident one that is silently wrong.