diff --git a/upstream/K-Dense-AI-scientific-agent-skills/catalogue/skills/lab-hardware-cad/SKILL.md b/upstream/K-Dense-AI-scientific-agent-skills/catalogue/skills/lab-hardware-cad/SKILL.md new file mode 100644 index 00000000..1cb65075 --- /dev/null +++ b/upstream/K-Dense-AI-scientific-agent-skills/catalogue/skills/lab-hardware-cad/SKILL.md @@ -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 `_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 --outdir DIR` | Run `build()`, export STEP and STL, write the provenance manifest | +| `gen.py --dxf [--dxf-z MM]` | Also slice a 2D DXF profile for laser cutting (default plane: mid-height) | +| `check.py facts ` | Validity, bounding box, volume, area, centre of mass, solid count | +| `check.py interfaces ` | Check every declared interface number against its standard; non-zero exit on failure | +| `check.py geometry ` | Evaluate the model's `checks()` gauges against the built solid — measured, not declared | +| `check.py probe --cyl D\|--box X,Y,Z --at ...` | One ad-hoc gauge: is this region clear of material, or filled with it | +| `check.py bores ` | 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 --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 --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. diff --git a/upstream/K-Dense-AI-scientific-agent-skills/catalogue/skills/lab-hardware-cad/assets/standards.json b/upstream/K-Dense-AI-scientific-agent-skills/catalogue/skills/lab-hardware-cad/assets/standards.json new file mode 100644 index 00000000..9a47a1fb --- /dev/null +++ b/upstream/K-Dense-AI-scientific-agent-skills/catalogue/skills/lab-hardware-cad/assets/standards.json @@ -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." + } + } +} diff --git a/upstream/K-Dense-AI-scientific-agent-skills/catalogue/skills/lab-hardware-cad/references/build123d-patterns.md b/upstream/K-Dense-AI-scientific-agent-skills/catalogue/skills/lab-hardware-cad/references/build123d-patterns.md new file mode 100644 index 00000000..90b3b63e --- /dev/null +++ b/upstream/K-Dense-AI-scientific-agent-skills/catalogue/skills/lab-hardware-cad/references/build123d-patterns.md @@ -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 — +- Introductory examples (builder vs algebra, selectors, fillets) — + +- Import/export reference — diff --git a/upstream/K-Dense-AI-scientific-agent-skills/catalogue/skills/lab-hardware-cad/references/fabrication-limits.md b/upstream/K-Dense-AI-scientific-agent-skills/catalogue/skills/lab-hardware-cad/references/fabrication-limits.md new file mode 100644 index 00000000..f92b969e --- /dev/null +++ b/upstream/K-Dense-AI-scientific-agent-skills/catalogue/skills/lab-hardware-cad/references/fabrication-limits.md @@ -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.1–0.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.8–3.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`. diff --git a/upstream/K-Dense-AI-scientific-agent-skills/catalogue/skills/lab-hardware-cad/references/labware-adapters.md b/upstream/K-Dense-AI-scientific-agent-skills/catalogue/skills/lab-hardware-cad/references/labware-adapters.md new file mode 100644 index 00000000..eb0839b1 --- /dev/null +++ b/upstream/K-Dense-AI-scientific-agent-skills/catalogue/skills/lab-hardware-cad/references/labware-adapters.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 — +- ANSI/SLAS 2-2004 (R2012) Height Dimensions — +- ANSI/SLAS 3-2004 (R2012) Bottom Outside Flange Dimensions — +- ANSI/SLAS 4-2004 (R2012) Well Positions — +- SLAS microplate standards overview — diff --git a/upstream/K-Dense-AI-scientific-agent-skills/catalogue/skills/lab-hardware-cad/references/microfluidics.md b/upstream/K-Dense-AI-scientific-agent-skills/catalogue/skills/lab-hardware-cad/references/microfluidics.md new file mode 100644 index 00000000..f5b70f0d --- /dev/null +++ b/upstream/K-Dense-AI-scientific-agent-skills/catalogue/skills/lab-hardware-cad/references/microfluidics.md @@ -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 --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. diff --git a/upstream/K-Dense-AI-scientific-agent-skills/catalogue/skills/lab-hardware-cad/references/optomechanics.md b/upstream/K-Dense-AI-scientific-agent-skills/catalogue/skills/lab-hardware-cad/references/optomechanics.md new file mode 100644 index 00000000..12b83fc0 --- /dev/null +++ b/upstream/K-Dense-AI-scientific-agent-skills/catalogue/skills/lab-hardware-cad/references/optomechanics.md @@ -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 — +- Thorlabs standard 30 mm cage plates — +- Thorlabs SM1 lens tube compatible cage plates — +- Post dimensions, beam heights, and vendor-specific thread conventions in this file are common + conventions rather than published standards. Confirm against the catalogue. diff --git a/upstream/K-Dense-AI-scientific-agent-skills/catalogue/skills/lab-hardware-cad/references/validation.md b/upstream/K-Dense-AI-scientific-agent-skills/catalogue/skills/lab-hardware-cad/references/validation.md new file mode 100644 index 00000000..cc88292b --- /dev/null +++ b/upstream/K-Dense-AI-scientific-agent-skills/catalogue/skills/lab-hardware-cad/references/validation.md @@ -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 --intent envelope --clearance --value = +``` + +## 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.