{"id":"lab-hardware-cad","name":"lab-hardware-cad","summary":"カスタムラボハードウェアをパラメトリックビルド123dモデルとして設計し、製造準備可能なSTEP、STL、DXFファイル(マイクロ流体チップや金型、オプトメカニカルマウントやブレッドボードアダプター、キュベットやマイクロプレートホルダー、チューブラック、動物行動リグ、3Dプリント機器器具など)をエクスポートできます。","body":"# Lab Hardware CAD\n\nDesign physical research hardware as **parametric Python source**, export STEP as the\nauthoritative artifact, and verify the result both numerically and visually before anything\nis fabricated.\n\nThe hard part of lab hardware is almost never the geometry. It is that the part must mate with\nequipment whose dimensions are fixed by a published standard or a vendor drawing. A holder that\nis 0.5 mm too wide does not fit the plate reader; a channel with the wrong aspect ratio collapses\nduring bonding; a mount whose bolt pattern is 25.4 mm instead of 25.0 mm will not reach the\noptical table. This skill exists to keep those numbers correct and checked.\n\n## When to use\n\nUse for any request to design, model, or fabricate a physical part for a lab: chip, mold, mount,\nadapter, holder, rack, bracket, enclosure, jig, fixture, arena, or maze. Also use to inspect or\nmodify an existing STEP file.\n\nDo **not** use for finite-element analysis, computational fluid dynamics, molecular structure,\nor scientific plotting. Those are different skills.\n\n## Setup\n\n```bash\nuv venv --python 3.12 .venv-labcad\nuv pip install --python .venv-labcad/bin/python \"build123d==0.11.1\" \"matplotlib>=3.8\"\n```\n\nbuild123d 0.11.1 requires Python >=3.10,<3.15 and pulls in the OpenCascade kernel through\n`cadquery-ocp-novtk`. The wheel is large; install once per project and reuse it.\n\nAll bundled scripts take `--help`. `check.py standards` runs without build123d installed.\n\n**Model files are executed, not parsed.** `gen.py`, `check.py`, and `snapshot.py` import a\n`*_model.py` and call its `build()`, which runs arbitrary Python in the current environment. That\nis inherent to parametric CAD — the source is the design. Only run model files authored in this\nsession or supplied by the user from a trusted location. If a model came from the internet, a\nshared drive, or an untrusted colleague, read it before running it and say that you did.\n\n## Required workflow\n\nFollow these steps in order. Steps 5 and 6 are not optional, and step 6 is not waived by step 5\npassing.\n\n### 1. Route to a device family\n\nRead the request, classify it, and load **exactly one** family reference. Do not load all four —\nthey are long, and mixing conventions between families is a common source of error.\n\n| If the part is | Load |\n| --- | --- |\n| A chip, mold, channel network, flow cell, gasket, or anything with fluid ports | `references/microfluidics.md` |\n| A mount, post, breadboard adapter, cage-system part, filter or sample holder in a beam path | `references/optomechanics.md` |\n| An adapter, insert, rack, or holder for plates, cuvettes, tubes, slides, or dishes | `references/labware-adapters.md` |\n| An arena, maze, head-fixation part, spout, tether, or extrusion-mounted enclosure for animal work | `references/behavior-rigs.md` |\n\nIf the part genuinely spans two families — a microfluidic chip that bolts to an optical table —\nload the family that owns the **critical interface**, then read only the interface section of the\nsecond. State in your response which family you routed to.\n\n### 2. Establish the interface dimensions before any geometry\n\nEvery part has at least one mating interface. Before writing code, write down for each interface:\n\n- the **source** of the dimension: a published standard, a vendor drawing, or a user measurement;\n- the **nominal value and tolerance**;\n- the **clearance or interference** you intend, and why.\n\nLook the number up in `assets/standards.json` or the family reference. **Never write an interface\ndimension from memory.** If the number is not in the standards file or the reference, ask the user\nfor the vendor drawing or the measurement rather than guessing. A guessed interface dimension is\nthe single most expensive failure mode in this skill.\n\nA feature that must *receive* a standardised component is sized against that component's\n**maximum material condition** — nominal plus its plus-tolerance — and only then given clearance.\nSized from nominal instead, it fits only the smaller half of conforming parts.\n\n```bash\npython scripts/check.py standards --list\npython scripts/check.py standards --show slas-microplate-footprint\n```\n\nThe bundled standard IDs (exact strings; do not guess variants): `slas-microplate-footprint`,\n`slas-microplate-height`, `slas-microplate-flange`, `slas-well-positions-96`,\n`slas-well-positions-384`, `slas-well-positions-1536`, `cuvette-standard-10mm`,\n`optical-breadboard-metric`, `optical-breadboard-imperial`, `cage-system-30mm`,\n`sm1-lens-tube-thread`.\n\nIf the part mates with nothing in this list, that is common and fine: declare no interfaces,\nand name every interface dimension with its source (user spec, vendor drawing, measurement) as\n**unchecked** in the report. Never declare against an unrelated standard to fill the gap — a\nfabricated declaration is worse than an honest \"nobody checked this\".\n\n### 3. Choose the process before choosing the geometry\n\nRead `references/fabrication-limits.md`. Process determines minimum wall, minimum feature,\nachievable tolerance, and whether the part survives autoclaving or contact with your solvent.\nFDM cannot hold ±0.05 mm; SLA resin is generally not safe for cell contact without post-cure and\ntesting. Record the process and material in the model docstring.\n\n### 4. Author a parametric model\n\nWrite `<part>_model.py`. The source is the authoritative artifact — **never hand-edit an exported\nSTEP file**, and never regenerate from a mesh.\n\nRequirements:\n\n- Every dimension that a user might change is a **module-level named constant** with units in the\n  name: `bore_d_mm`, `wall_t_mm`, `post_h_mm`. No bare numbers in the body except 0, 1, and 2.\n- Expose `build() -> Part`. `gen.py` calls it.\n- Group parameters into an `INTERFACE` block (dimensions fixed by a standard, annotated with the\n  standard ID) and a `DESIGN` block (dimensions you are free to choose).\n- **Derive every computed dimension inside a function**, never at module level, so `--param`\n  overrides actually reach it.\n- Declare an `interfaces()` function returning the dimensions the part must fit, each with its\n  standard ID and intent. This is what makes the interface machine-checkable in step 5.\n  `intent` is `\"envelope\"` when the feature must **accept** any conforming part (a pocket, bore,\n  or slot — checked one-sided at maximum material condition plus your clearance) and `\"match\"`\n  when this part must itself conform (symmetric band). `clearance` is the total intended\n  clearance in mm and must be non-negative. Declare only dimensions that constrain *this part's\n  mating features* — a property of the mating equipment (a table's edge border, a typical plate\n  thickness) is not an interface of yours. If no bundled standard applies, return `[]`.\n- Declare a `checks()` function of **go/no-go gauges measured from the built solid**: a `clear`\n  region for everything that must pass through or fit in (screw shafts, beam corridors, the\n  mating part at maximum material condition dropping into its pocket), a `material` region for\n  everything that must remain (a ridge, a ledge, a screw seat), and a `bbox_*` bound for every\n  size limit the user stated. Map **every geometric requirement in the request** to one entry;\n  these catch the errors that `is_valid`, the bounding box, and declared numbers cannot see.\n  `gen.py` runs them on every generation and fails the build when one fails. Schema and worked\n  examples: `references/build123d-patterns.md`.\n- Put the process, material, and every interface source in the module docstring.\n\n```python\n\"\"\"SLAS microplate carrier for a custom stage insert.\n\nProcess: FDM, PETG, 0.2 mm layer.  Tolerance budget +/-0.3 mm.\nInterfaces:\n  - Plate pocket: ANSI/SLAS 1-2004 (R2012) footprint 127.76 x 85.48 mm, +/-0.25.\n  - Stage bolts: user-measured, 40.0 mm centres (drawing in docs/stage.pdf).\n\"\"\"\nfrom build123d import *\n\n# --- INTERFACE (fixed by standard; do not tune) ---\nplate_l_mm = 127.76   # ANSI/SLAS 1-2004 nominal\nplate_w_mm = 85.48    # ANSI/SLAS 1-2004 nominal\nplate_tol_mm = 0.25   # ANSI/SLAS 1-2004; the pocket is sized to nominal + this\n# --- DESIGN (free) ---\npocket_clearance_mm = 0.40   # per-side; FDM, see fabrication-limits.md\nwall_t_mm = 3.0\nfloor_t_mm = 2.5\nbody_h_mm = 12.0\n\n\ndef pocket_mm() -> tuple[float, float]:\n    \"\"\"Pocket at the plate's maximum material condition plus clearance per side.\n\n    A pocket sized from nominal jams on roughly half of conforming plates.\n    \"\"\"\n    growth = plate_tol_mm + 2 * pocket_clearance_mm\n    return plate_l_mm + growth, plate_w_mm + growth\n\n\ndef interfaces() -> list[dict]:\n    \"\"\"What this part must fit. `check.py interfaces` verifies every entry.\"\"\"\n    pocket_l, pocket_w = pocket_mm()\n    return [\n        {\"feature\": \"plate pocket length\", \"standard\": \"slas-microplate-footprint\",\n         \"dimension\": \"footprint_length\", \"value\": pocket_l,\n         \"intent\": \"envelope\", \"clearance\": 2 * pocket_clearance_mm},\n        {\"feature\": \"plate pocket width\", \"standard\": \"slas-microplate-footprint\",\n         \"dimension\": \"footprint_width\", \"value\": pocket_w,\n         \"intent\": \"envelope\", \"clearance\": 2 * pocket_clearance_mm},\n    ]\n\n\ndef checks() -> list[dict]:\n    \"\"\"Gauges measured from the built solid. Sized from the REQUIREMENT's numbers\n    (plate MMC, the user's height limit), not from the pocket parameters, so a\n    wrong parameter cannot shrink the gauge to match the wrong geometry.\"\"\"\n    depth = body_h_mm - floor_t_mm\n    return [\n        {\"feature\": \"plate at MMC drops into the pocket\",\n         \"clear\": {\"box\": (plate_l_mm + plate_tol_mm, plate_w_mm + plate_tol_mm, depth),\n                   \"at\": [(0.0, 0.0, floor_t_mm + depth / 2)]}},\n        {\"feature\": \"under 15 mm for the stage\", \"bbox_z\": {\"max\": 15.0}},\n    ]\n\n\ndef build() -> Part:\n    pocket_l, pocket_w = pocket_mm()\n    with BuildPart() as carrier:\n        Box(pocket_l + 2 * wall_t_mm, pocket_w + 2 * wall_t_mm, body_h_mm,\n            align=(Align.CENTER, Align.CENTER, Align.MIN))\n        with Locations((0, 0, floor_t_mm)):\n            Box(pocket_l, pocket_w, body_h_mm, mode=Mode.SUBTRACT,\n                align=(Align.CENTER, Align.CENTER, Align.MIN))\n    return carrier.part\n```\n\nSee `references/build123d-patterns.md` for the builder-vs-algebra choice, the `interfaces()`\ncontract, sketching, selectors, fillets, and threaded-insert bores.\n\n### 5. Generate and run the checks\n\n```bash\npython scripts/gen.py carrier_model.py --outdir out/\npython scripts/check.py facts out/carrier.step\npython scripts/check.py interfaces out/carrier.manifest.json\npython scripts/check.py geometry out/carrier.step --model carrier_model.py\n```\n\n`gen.py` also evaluates the model's `checks()` gauges against the solid it just built, prints\neach PASS/FAIL, records them in the manifest, and exits non-zero on a failure — so a part that\nviolates its own declared geometry never silently becomes an artifact. `check.py geometry`\nre-runs the same gauges against the exported STEP, which is the authoritative artifact.\n\n`out/` is a scratch convention, not a requirement. When the user asked for deliverables in a\nspecific place, generate there (`--outdir .`) or copy the STEP, manifest, and DXF to it before\nfinishing — a deliverable that exists only inside `out/` has not been delivered.\n\n`gen.py` writes `carrier.step` (authoritative), `carrier.stl` (mesh preview and printing), and\n`carrier.manifest.json` recording the source hash, resolved parameters, declared interfaces,\nlibrary versions, and measured bounding box, volume, and validity. The manifest is the provenance\nrecord — keep it with the artifact.\n\n`check.py facts` reports `is_valid`, bounding box, volume, surface area, centre of mass, and\nsolid count. A part that reports `is_valid: false` is broken geometry; fix the source before going\nfurther.\n\n`check.py interfaces` evaluates every entry the model declared against the standards database\nand exits non-zero on failure. **Be clear about what it does and does not verify:** it checks the\n*declared numbers* — catching a transcribed dimension, the wrong standard, and\nnominal-instead-of-MMC sizing — but it never measures the built geometry, and a value computed\nfrom the same constants it is checked against passes with zero headroom by construction. Do not\ncite it as evidence the geometry is right; `facts` and the snapshot are the geometry checks.\nAn empty declaration list passes: a part that mates with nothing in the bundled database has\nnothing to declare, and its interface dimensions are instead named as unchecked in the report.\n\nUse `interfaces` rather than `check.py fit` for anything internal — a pocket, bore, or slot does\nnot appear in the part's outer bounding box, which is what `fit` measures. Reach for `fit` only\nto check one number by hand (`--value footprint_length=128.81`), or when the part's own outline\nis the interface, such as a gasket cut to a plate footprint.\n\nFor assemblies, check that parts do not interfere:\n\n```bash\npython scripts/check.py clearance out/carrier.step out/lid.step --min 0.3\n```\n\n### 6. Snapshot and actually look at it\n\n```bash\npython scripts/snapshot.py out/carrier.step --out out/carrier.png\n```\n\nThen **read the PNG**. This step is mandatory after every generation and every modification.\nDeterministic checks passing is not a reason to skip it: `is_valid` and a correct bounding box are\nboth fully consistent with a pocket cut on the wrong face, a boss placed outside the body, or a\nfillet that ate a feature. Those errors are obvious in a picture and invisible in the numbers.\n\nKnow the render's limits too. A feature much smaller than the frame — a 0.3 mm mold ridge on a\n40 mm part, a counterbore step on a plate — may not be decidable from the views at all. Do not\nreport seeing something the image cannot resolve; that is worse than not looking. For such\nfeatures the skill has instruments: `check.py bores` prints every cylindrical face (diameter,\naxis, position, span, sweep) so you can reconcile the drilling against the model's intent, and\n`check.py probe` answers a one-off \"is this region clear / is material present here\" without\nediting the model. Cite the measured numbers; report from the picture only what the picture\nactually shows.\n\nThe six views are true orthographic projections, and the outlines are the model's real edges drawn\n**without hidden-line removal**. So a circle visible \"through\" material is a bore on the far side,\nnot a window — the part is not transparent. Read it that way rather than reporting a hole that\nis not there.\n\nState in your response what you saw in the snapshot, not merely that you generated one.\n\n### 7. Repair through the source\n\nIf any check fails, edit the parameters or the model code, rerun `gen.py`, and rerun **both**\nstep 5 and step 6. Never patch the STEP.\n\n### 8. Report before fabrication\n\nWork through `references/validation.md` and give the user: the process and material, every\ninterface dimension with its source and tolerance, the clearances chosen, what the snapshot showed,\nand any check that did not pass.\n\nFlag explicitly every interface the automatic check could not cover — a vendor drawing, a user\nmeasurement, a standard not in the bundled database. `check.py interfaces` reports only what the\nmodel declared against a known standard, so silence there is not confirmation; a dimension nobody\ncould check has to be named as such.\n\n## Units\n\nbuild123d is unitless internally and everything in this skill is **millimetres and degrees**.\n`export_step` is called with `Unit.MM`. Imperial hardware appears throughout optomechanics\n(1/4-20 screws, 1 inch grids, SM1 threads); convert to millimetres in a single named constant at\nthe point of definition and never mix systems inside an expression. 1 inch is exactly 25.4 mm, and\na 25 mm metric optical grid is **not** interchangeable with a 1 inch imperial grid — the error\naccumulates to 1.6 mm over four holes.\n\n## Tolerances and fits\n\nA nominal dimension is not a fit. Every mating dimension needs a deliberate clearance chosen from\nthe process tolerance in `references/fabrication-limits.md`. Common defaults, per side:\n\n| Fit | FDM | SLA | CNC |\n| --- | --- | --- | --- |\n| Free-sliding (plate in a pocket) | 0.40 mm | 0.20 mm | 0.10 mm |\n| Located but removable | 0.25 mm | 0.10 mm | 0.05 mm |\n| Press / interference | -0.05 mm | -0.03 mm | -0.02 mm |\n\nThese are starting points for a first article, not guarantees. Say so when you report them, and\nrecommend printing a test coupon of the critical interface before committing to a full part.\n\n## Scientific caveats\n\n- **Material compatibility governs.** A geometrically perfect part in the wrong polymer fails in\n  service: autoclave cycles distort PLA, many solvents craze acrylic, and uncured SLA resin is\n  cytotoxic. Check `references/fabrication-limits.md` before recommending a material for anything\n  contacting cells, tissue, solvents, or heat.\n- **Optical parts have non-geometric requirements.** Autofluorescence, surface roughness, and\n  stray-light scatter are not visible in a STEP file. Black resin is not automatically low-scatter.\n- **Vendor labware varies.** The SLAS standards fix the plate footprint but not well geometry,\n  skirt profile, or lid fit, and consumable tubes differ between suppliers. Design to the standard\n  where one exists; otherwise require a measurement.\n- **A passing bounding box is not a passing part.** `fit` checks the dimensions it is given. It\n  cannot see a missing feature, and it does not replace the snapshot.\n\n## References\n\n| File | Contents |\n| --- | --- |\n| `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 |\n| `references/optomechanics.md` | Breadboard grids and screw clearances, post and pedestal heights, 30 mm cage geometry, SM lens-tube threads, beam height |\n| `references/labware-adapters.md` | ANSI/SLAS 1-4 microplate dimensions, cuvettes, tubes, slides, dishes, deck and stage constraints |\n| `references/behavior-rigs.md` | Arena and maze geometry, head-fixation interfaces, spouts and ports, T-slot extrusion, cleaning and durability |\n| `references/fabrication-limits.md` | Process tolerances, minimum walls and features, clearance and thread inserts, materials, autoclave and solvent and biocompatibility |\n| `references/validation.md` | Pre-fabrication checklist and the failure modes each item catches |\n| `references/build123d-patterns.md` | build123d 0.11.1 API cookbook: builder vs algebra, sketches, selectors, joints, exports |\n\n## Scripts\n\n| Command | Purpose |\n| --- | --- |\n| `gen.py <model.py> --outdir DIR` | Run `build()`, export STEP and STL, write the provenance manifest |\n| `gen.py <model.py> --dxf [--dxf-z MM]` | Also slice a 2D DXF profile for laser cutting (default plane: mid-height) |\n| `check.py facts <step>` | Validity, bounding box, volume, area, centre of mass, solid count |\n| `check.py interfaces <manifest\\|model.py>` | Check every declared interface number against its standard; non-zero exit on failure |\n| `check.py geometry <model.py\\|step --model M>` | Evaluate the model's `checks()` gauges against the built solid — measured, not declared |\n| `check.py probe <step> --cyl D\\|--box X,Y,Z --at ...` | One ad-hoc gauge: is this region clear of material, or filled with it |\n| `check.py bores <step>` | Census of every cylindrical face: diameter, axis, position, span, sweep |\n| `check.py fit --standard ID --value DIM=MM` | Check one dimension by hand, or a part whose outer envelope is the interface |\n| `check.py clearance <a> <b> --min MM` | Minimum distance between two solids; detects interference |\n| `check.py standards [--list\\|--show ID]` | Browse the bundled standards data (standard library only) |\n| `snapshot.py <step> --out PNG` | Six-view orthographic and isometric render for visual review |\n\nAll commands accept `--json` for machine-readable output and write progress to stderr.\n`check.py standards`, and `check.py interfaces` on a manifest, run without build123d installed.","author":"@K-Dense-AI","ownerProfile":null,"authorContacts":null,"sourceUrl":"https://github.com/K-Dense-AI/scientific-agent-skills/tree/main/skills/lab-hardware-cad","license":"MIT","category":"coding","lang":"en","tokens":4871,"stars":0,"calls30d":1,"claimed":false,"visibility":"public","origin":"crawler","version":"0.1.0","createdAt":"2026-08-22","updatedAt":"2026-08-22","files":[{"path":"assets/standards.json","size":13658,"sha256":"ac081483e0d70fb41cd37821093bd3602fc081a54384abb77e49148831d153d9"},{"path":"references/behavior-rigs.md","size":7393,"sha256":"4e1500f623067571244f361c97f0f13ae530055ad7daa88df06709474e1a6d41"},{"path":"references/build123d-patterns.md","size":16888,"sha256":"2d536d09771772c9140653e7ef86df35351d33ad4fa6c45d4954a8fd5290b256"},{"path":"references/fabrication-limits.md","size":8075,"sha256":"a4d87f56bb95db8bb7ffd267f38c4337841b7735198527fecaf0ac6d2bcaedb9"},{"path":"references/labware-adapters.md","size":9529,"sha256":"6e54cabb0ad4eea78411599d146ab1a66b9378b3407cfcb76fb7e7d3d00ab71e"},{"path":"references/microfluidics.md","size":8451,"sha256":"6b1554443e94690389b560127620c3c459635e0eadb4418ef770eecb5d257496"},{"path":"references/optomechanics.md","size":7200,"sha256":"89774b6645fc61fdb7700e563191552d9f2b3a934193216bbbb7ada61b31f217"},{"path":"references/validation.md","size":6699,"sha256":"ca1fa6e45247cbd01e042a555c8b2c393eda8fdf9710a6bc3160b4d8bd0d2751"},{"path":"scripts/check.py","size":27155,"sha256":"f6f1cd304c327a237f5e079c13a208f7ee3bb1bebcc9d08735ababaf6235557d"},{"path":"scripts/_common.py","size":25023,"sha256":"384732d6d14b039aab3f4e2a1f9867f1f6d0ee394d1699ef19e64a3f6ea56c2c"},{"path":"scripts/gen.py","size":10764,"sha256":"d159d22429d2983921726e4ea2256935a5a63eca030463fe5ab45416ec3a7f27"},{"path":"scripts/snapshot.py","size":10909,"sha256":"4d6887482d83df8f20093238597d5b8053294ab7e22de0cad24c5ae32e898e21"}],"requires":{"mcp":[],"tools":["Read Write Edit Bash Glob Grep"]},"safety":{"flags":[{"code":"net.endpoints","kind":"exfiltration","excerpt":"build123d.readthedocs.io, spectrecology.com, www.slas.org, www.thorlabs.com","message":"bundled scripts reach 4 external host(s)","severity":"warn"}],"scannedAt":"2026-08-22","hasScripts":true,"networkEndpoints":["build123d.readthedocs.io","spectrecology.com","www.slas.org","www.thorlabs.com"]}}