Compare commits

..
Author SHA1 Message Date
promptadmin fda2d80af5 [upstream-sync] skills/stable-baselines3/references/vectorized_envs.md from K-Dense-AI/scientific-agent-skills@72d742e1 [prompt] 2026-08-29 17:06:20 +00:00
promptadmin 6dfa33d85b [upstream-sync] skills/stable-baselines3/references/callbacks.md from K-Dense-AI/scientific-agent-skills@72d742e1 [prompt] 2026-08-29 17:06:17 +00:00
promptadmin 7b8888cb03 [upstream-sync] skills/stable-baselines3/SKILL.md from K-Dense-AI/scientific-agent-skills@72d742e1 [unknown] 2026-08-29 17:06:15 +00:00
promptadmin 0eae1b9c41 [upstream-sync] skills/rowan/references/workflow_catalog.md from K-Dense-AI/scientific-agent-skills@72d742e1 [unknown] 2026-08-29 17:06:12 +00:00
promptadmin 40fc3e3776 [upstream-sync] skills/rowan/references/troubleshooting.md from K-Dense-AI/scientific-agent-skills@72d742e1 [prompt] 2026-08-29 17:06:10 +00:00
promptadmin c612f8708b [upstream-sync] skills/rowan/references/end_to_end_example.md from K-Dense-AI/scientific-agent-skills@72d742e1 [prompt] 2026-08-29 17:06:08 +00:00
promptadmin 44a925eec0 [upstream-sync] skills/rowan/references/batch_and_webhooks.md from K-Dense-AI/scientific-agent-skills@72d742e1 [prompt] 2026-08-29 17:06:06 +00:00
promptadmin 8c945e126c [upstream-sync] skills/rowan/SKILL.md from K-Dense-AI/scientific-agent-skills@72d742e1 [prompt] 2026-08-29 17:06:03 +00:00
promptadmin fd4c52a855 Merge pull request '[Upstream sync] K-Dense-AI/scientific-agent-skills (github) — 0 added, 8 modified' (#45) from upstream-sync/scientific-agent-skills-20260816-336c4f-xhnj into main
Reviewed-on: #45
2026-08-21 14:39:29 +00:00
promptadmin 622fbcad6b Merge pull request '[Upstream sync] K-Dense-AI/scientific-agent-skills (github) — 0 added, 2 modified' (#46) from upstream-sync/scientific-agent-skills-20260817-28f560-hcyg into main
Reviewed-on: #46
2026-08-21 14:39:16 +00:00
promptadmin 51b31fa16b Merge pull request '[Upstream sync] mims-harvard/ToolUniverse (github) — 0 added, 3 modified' (#49) from upstream-sync/tooluniverse-20260818-4d1423-lgxp into main
Reviewed-on: #49
2026-08-21 14:38:43 +00:00
promptadmin fd0a4b53fd Merge pull request '[Upstream sync] K-Dense-AI/scientific-agent-skills (github) — 0 added, 1 modified' (#52) from upstream-sync/scientific-agent-skills-20260819-de66e1-hkiy into main
Reviewed-on: #52
2026-08-21 14:38:29 +00:00
promptadmin 6e80bc9384 Merge pull request '[Upstream sync] K-Dense-AI/scientific-agent-skills (github) — 0 added, 1 modified' (#53) from upstream-sync/scientific-agent-skills-20260820-390f51-ongp into main
Reviewed-on: #53
2026-08-21 14:38:09 +00:00
promptadmin 6a176003b4 Merge pull request '[Upstream sync] mims-harvard/ToolUniverse (github) — 62 added, 0 modified' (#54) from upstream-sync/tooluniverse-20260820-1aaaf0-qirw into main
Reviewed-on: #54
2026-08-21 14:37:37 +00:00
promptadmin ae0179ea23 [upstream-sync] README.md from K-Dense-AI/scientific-agent-skills@390f5146 [catalogue] 2026-08-20 04:32:39 +00:00
promptadmin 7ac5f0e4be [upstream-sync] AGENTS.md from K-Dense-AI/scientific-agent-skills@de66e10c [unknown] 2026-08-19 22:31:43 +00:00
promptadmin 0b7324d180 [upstream-sync] skills/tooluniverse-phylogenetics/SKILL.md from mims-harvard/ToolUniverse@4d14233e [catalogue] 2026-08-18 04:26:05 +00:00
promptadmin ca8242906e [upstream-sync] skills/tooluniverse-biomedical-fact-lookup/SKILL.md from mims-harvard/ToolUniverse@4d14233e [unknown] 2026-08-18 04:26:02 +00:00
promptadmin dcb71b5f48 [upstream-sync] skills/tooluniverse-adverse-event-detection/TOOL_REFERENCE.md from mims-harvard/ToolUniverse@4d14233e [unknown] 2026-08-18 04:26:00 +00:00
promptadmin e34cd2906b [upstream-sync] docs/security-report.md from K-Dense-AI/scientific-agent-skills@28f5603b [catalogue] 2026-08-17 10:22:37 +00:00
promptadmin c61e19ee1c [upstream-sync] docs/security-report.json from K-Dense-AI/scientific-agent-skills@28f5603b [catalogue] 2026-08-17 10:22:33 +00:00
promptadmin f830833cd0 [upstream-sync] skills/lab-hardware-cad/references/validation.md from K-Dense-AI/scientific-agent-skills@336c4f83 [unknown] 2026-08-16 04:18:28 +00:00
promptadmin 4bcd681807 [upstream-sync] skills/lab-hardware-cad/references/optomechanics.md from K-Dense-AI/scientific-agent-skills@336c4f83 [unknown] 2026-08-16 04:18:26 +00:00
promptadmin 6648b0e79b [upstream-sync] skills/lab-hardware-cad/references/microfluidics.md from K-Dense-AI/scientific-agent-skills@336c4f83 [unknown] 2026-08-16 04:18:24 +00:00
promptadmin f25bc25ee6 [upstream-sync] skills/lab-hardware-cad/references/labware-adapters.md from K-Dense-AI/scientific-agent-skills@336c4f83 [unknown] 2026-08-16 04:18:22 +00:00
promptadmin d009ea87ef [upstream-sync] skills/lab-hardware-cad/references/fabrication-limits.md from K-Dense-AI/scientific-agent-skills@336c4f83 [unknown] 2026-08-16 04:18:20 +00:00
promptadmin b6bc4d2d42 [upstream-sync] skills/lab-hardware-cad/references/build123d-patterns.md from K-Dense-AI/scientific-agent-skills@336c4f83 [unknown] 2026-08-16 04:18:18 +00:00
promptadmin df84a99a63 [upstream-sync] skills/lab-hardware-cad/assets/standards.json from K-Dense-AI/scientific-agent-skills@336c4f83 [unknown] 2026-08-16 04:18:16 +00:00
promptadmin 2509cbc753 [upstream-sync] skills/lab-hardware-cad/SKILL.md from K-Dense-AI/scientific-agent-skills@336c4f83 [catalogue] 2026-08-16 04:18:14 +00:00
23 changed files with 5198 additions and 2632 deletions
@@ -2,9 +2,9 @@
title: "Repository Guidance"
task: ""
lineage_type: import
upstream_source: https://github.com/K-Dense-AI/scientific-agent-skills/blob/991bd993/AGENTS.md
upstream_sha: 991bd993
imported_at: 2026-08-08
upstream_source: https://github.com/K-Dense-AI/scientific-agent-skills/blob/de66e10c/AGENTS.md
upstream_sha: de66e10c
imported_at: 2026-08-19
prompt_class: unknown
upstream_changes: accepted
author: upstream
@@ -38,7 +38,14 @@ The general-purpose skills that do exist are narrow output-format helpers (`docx
## Layout
The repository root is an [Agent Plugins](https://agent-plugins.org/) 1.0.0 package: `plugin.json`
plus the portable `skills/` tree. Keep `plugin.json` valid against the Agent Plugins manifest
schema, and keep its `version` identical to `pyproject.toml` `[project].version`. Do not add
non-portable top-level fields to `plugin.json` (no inline MCP, hooks, or client-only keys — use
`mcp.json` or a reverse-domain `extensions` namespace if those are ever needed).
```text
plugin.json # Agent Plugins manifest (repo root)
skills/<skill-name>/
├── SKILL.md # required
├── references/ # optional: long documentation, loaded only when needed
@@ -46,8 +53,8 @@ skills/<skill-name>/
└── assets/ # optional: templates and static resources
```
Only `SKILL.md` is required. Reference other files with relative paths from the skill root, kept
one level deep.
Only `SKILL.md` is required inside each skill. Reference other files with relative paths from the
skill root, kept one level deep.
**Tests never live under `skills/`.** A skill directory ships only what an agent loads. Checks for a
skill's scripts and structure go in the repository-level suite instead:
@@ -58,9 +65,9 @@ tests/<skill-name>/ # same name as the skill directory
└── fixtures/ # optional test data
```
**Diagrams never live under `skills/` either.** Every skill has one generated workflow diagram at
`docs/images/<skill-name>.png`, produced by `scripts/generate_skill_image.py` and kept in step with
the skill's documentation — see [Skill diagrams](#skill-diagrams).
**Diagrams never live under `skills/` either.** A skill may have a generated workflow diagram at
`docs/images/<skill-name>.png`, produced by `scripts/generate_skill_image.py`. Diagrams are optional
— see [Skill diagrams](#skill-diagrams).
Tests reach their skill through an explicit anchor, never a relative walk:
@@ -79,11 +86,6 @@ SKILL_ROOT = Path(__file__).resolve().parents[2] / "skills" / "<skill-name>"
5. If the skill ships `scripts/`, put their tests in **`tests/<name>/`** — never in the skill
directory. Fixtures go in `tests/<name>/fixtures/`.
6. Validate and scan (below).
7. Generate the skill's diagram — a new skill without `docs/images/<name>.png` is incomplete:
```bash
uv run python scripts/generate_skill_image.py --skill <name>
```
```markdown
---
@@ -121,16 +123,6 @@ Use this skill when...
5. Re-run any example, command, or script you touched, plus `tests/<name>/` if that suite exists.
Suites check that `metadata.version` is present and quoted, not what it equals, so a version bump
never needs a matching test edit.
6. **Regenerate the diagram in the same change** whenever the edit changes what the skill does or
how its workflow runs — the picture is generated from `SKILL.md` and `references/`, so it goes
stale silently. The command overwrites `docs/images/<name>.png` in place:
```bash
uv run python scripts/generate_skill_image.py --skill <name>
```
A typo fix, a link repair, or a version bump alone does not need a new image.
## Frontmatter
`SKILL.md` starts with YAML frontmatter. **Only these six fields are allowed** — the spec defines a
@@ -337,10 +329,11 @@ touch the shared contract.
## Skill diagrams
Every skill carries one generated workflow diagram at `docs/images/<skill-name>.png`. Creating a
skill means creating its image; changing what a skill does means regenerating it. The image is not
optional decoration — it is derived from the documentation, so an out-of-date one misrepresents the
skill.
A skill may carry a generated workflow diagram at `docs/images/<skill-name>.png`. Diagrams are
optional: neither a new skill nor a change to an existing one is blocked on having or refreshing an
image, and no CI check enforces them. If you do ship one, note that it is derived from the
documentation, so regenerate it when the skill's workflow changes rather than leaving a picture that
misrepresents the skill.
`scripts/generate_skill_image.py` is local repository tooling, standard library only, and runs in
two stages on one `OPENROUTER_API_KEY` (environment variable, repository `.env`, or `--api-key`):
@@ -380,12 +373,13 @@ hand-tuning one skill's prompt, so the set stays visually consistent.
- `metadata.version` exists, is quoted, and is bumped if you changed an existing skill.
- `metadata` is a block mapping; `openclaw` / `hermes` blocks are nested mappings.
- `uv run skills-ref validate skills/<name>` passes.
- If the collection version changes, `plugin.json` `version` matches `pyproject.toml`.
- `uv run --with pytest python -m pytest tests/_meta -q` passes — this is what CI blocks on, and it
catches a missing suite, a missing `skill-requirements.toml` entry, a broken local link, and a
leaked local path.
catches a missing suite, a missing `skill-requirements.toml` entry, a broken local link, a
leaked local path, and a drifted Agent Plugins manifest.
- If the skill ships `scripts/`: a suite exists at `tests/<name>/`, a `[skills.<name>]` entry exists
in `tests/skill-requirements.toml`, and `python tests/run_all.py --isolated <name>` passes.
- `docs/images/<name>.png` exists, and was regenerated if the change altered what the skill does.
Its labels are spelled correctly and its arrows point where they should.
- If the skill ships `docs/images/<name>.png`, its labels are spelled correctly and its arrows point
where they should. The image itself is optional.
- Examples and scripts are tested, or clearly marked illustrative.
- No secrets or private data; scan results clean or explained in the PR.
@@ -2,9 +2,9 @@
title: "Scientific Agent Skills"
task: ""
lineage_type: import
upstream_source: https://github.com/K-Dense-AI/scientific-agent-skills/blob/991bd993/README.md
upstream_sha: 991bd993
imported_at: 2026-08-08
upstream_source: https://github.com/K-Dense-AI/scientific-agent-skills/blob/390f5146/README.md
upstream_sha: 390f5146
imported_at: 2026-08-20
prompt_class: catalogue
upstream_changes: accepted
author: upstream
@@ -14,34 +14,29 @@ validated: false
# Scientific Agent Skills
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE.md)
[![Version](https://img.shields.io/badge/Version-2.62.0-blue.svg)](pyproject.toml)
[![Skills](https://img.shields.io/badge/Skills-159-brightgreen.svg)](#-whats-included)
[![Version](https://img.shields.io/badge/Version-2.64.0-blue.svg)](pyproject.toml)
[![Skills](https://img.shields.io/badge/Skills-163-brightgreen.svg)](#-whats-included)
[![Databases](https://img.shields.io/badge/Databases-100%2B-orange.svg)](#-whats-included)
[![Agent Skills](https://img.shields.io/badge/Standard-Agent_Skills-blueviolet.svg)](https://agentskills.io/)
[![Agent Plugins](https://img.shields.io/badge/Standard-Agent_Plugins-0A7A72.svg)](https://agent-plugins.org/)
[![Security Scan](https://github.com/K-Dense-AI/scientific-agent-skills/actions/workflows/security-scan.yml/badge.svg)](https://github.com/K-Dense-AI/scientific-agent-skills/actions/workflows/security-scan.yml)
[![Skill Tests](https://github.com/K-Dense-AI/scientific-agent-skills/actions/workflows/skill-tests.yml/badge.svg)](https://github.com/K-Dense-AI/scientific-agent-skills/actions/workflows/skill-tests.yml)
[![Works with](https://img.shields.io/badge/Works_with-Cursor_|_Claude_Code_|_Codex_|_Google_Antigravity-blue.svg)](#-getting-started)
[![X](https://img.shields.io/badge/Follow_on_X-%40k__dense__ai-000000?logo=x)](https://x.com/k_dense_ai)
[![LinkedIn](https://img.shields.io/badge/LinkedIn-K--Dense_Inc.-0A66C2?logo=linkedin)](https://www.linkedin.com/company/k-dense-inc)
[![YouTube](https://img.shields.io/badge/YouTube-K--Dense_Inc.-FF0000?logo=youtube)](https://www.youtube.com/@K-Dense-Inc)
## Star History
<a href="https://www.star-history.com/?repos=K-Dense-AI%2Fscientific-agent-skills&type=date&legend=top-left">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=K-Dense-AI/scientific-agent-skills&type=date&theme=dark&legend=top-left&sealed_token=rL_5GLS9f4Fbyr1_VYZLGMF-8Rr6ZlWNaYNecajc52QSQq6KL7HrzSea_tGQGy1mBMXgVvAUMSIYAc0w39si9v5Up1RIw74-UDGZg_9HvH_chiyS0Njf-5tebtPh1LJjXTG6mH5Iv2pMJNivgfPsyB-oOgbaIV3uSc7DzSeZFCTE4WOcHX4y2BR76k5g" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=K-Dense-AI/scientific-agent-skills&type=date&legend=top-left&sealed_token=rL_5GLS9f4Fbyr1_VYZLGMF-8Rr6ZlWNaYNecajc52QSQq6KL7HrzSea_tGQGy1mBMXgVvAUMSIYAc0w39si9v5Up1RIw74-UDGZg_9HvH_chiyS0Njf-5tebtPh1LJjXTG6mH5Iv2pMJNivgfPsyB-oOgbaIV3uSc7DzSeZFCTE4WOcHX4y2BR76k5g" />
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=K-Dense-AI/scientific-agent-skills&type=date&legend=top-left&sealed_token=rL_5GLS9f4Fbyr1_VYZLGMF-8Rr6ZlWNaYNecajc52QSQq6KL7HrzSea_tGQGy1mBMXgVvAUMSIYAc0w39si9v5Up1RIw74-UDGZg_9HvH_chiyS0Njf-5tebtPh1LJjXTG6mH5Iv2pMJNivgfPsyB-oOgbaIV3uSc7DzSeZFCTE4WOcHX4y2BR76k5g" />
</picture>
</a>
[![Reddit](https://img.shields.io/badge/Reddit-u%2F--k--dense---FF4500?logo=reddit&logoColor=white)](https://www.reddit.com/user/-k-dense-/)
> **🔔 Claude Scientific Skills is now Scientific Agent Skills.** Same skills, broader compatibility — now works with any AI agent that supports the open [Agent Skills](https://agentskills.io/) standard, not just Claude.
> **New: [K-Dense BYOK](https://github.com/K-Dense-AI/k-dense-byok)** — A free, open-source AI co-scientist that runs on your desktop, powered by Scientific Agent Skills. Bring your own API keys, pick from 40+ models, and get a full research workspace with web search, file handling, 100+ scientific databases, and access to all 159 skills in this repo. Your data stays on your computer, and you can optionally scale to cloud compute via [Modal](https://modal.com/) for heavy workloads. [Get started here.](https://github.com/K-Dense-AI/k-dense-byok)
> **New: [K-Dense BYOK](https://github.com/K-Dense-AI/k-dense-byok)** — A free, open-source AI co-scientist that runs on your desktop, powered by Scientific Agent Skills. Bring your own API keys, pick from 40+ models, and get a full research workspace with web search, file handling, 100+ scientific databases, and access to all 161 skills in this repo. Your data stays on your computer, and you can optionally scale to cloud compute via [Modal](https://modal.com/) for heavy workloads. [Get started here.](https://github.com/K-Dense-AI/k-dense-byok)
> **Stay up to date:** Follow K-Dense on [X](https://x.com/k_dense_ai), [LinkedIn](https://www.linkedin.com/company/k-dense-inc), and [YouTube](https://www.youtube.com/@K-Dense-Inc) for new skills, release announcements, walkthroughs, research workflow demos, and examples you can use with your own AI agent.
> **🎥 Live webinar — [Getting Started with K-Dense BYOK](https://luma.com/nucztxt5)** · Tuesday, August 25, 2026 · 2:00 PM PT / 5:00 PM ET · Online, free
> Join us for a hands-on walkthrough of [K-Dense BYOK](https://github.com/K-Dense-AI/k-dense-byok), our free, open-source AI co-scientist that runs locally on your own machine and is powered by Scientific Agent Skills. We'll show you how to set it up, bring your own API keys, and run real research workflows with these skills. No prior technical experience needed, and questions are welcome throughout. **[Save your spot →](https://luma.com/nucztxt5)**
A comprehensive collection of **159 ready-to-use scientific and research skills** (covering cancer genomics, individual-level 1000 Genomes queries, hosted regulatory-sequence prediction, live pathogen-variant surveillance, analytical method validation, PK/PD modelling and dose selection, full-text biomedical and regulatory literature retrieval, drug-target binding, molecular dynamics, RNA velocity, geospatial science, time series forecasting, scientific ML resource discovery via Hugging Science, 78+ scientific databases, and more) for any AI agent that supports the open [Agent Skills](https://agentskills.io/) standard, created by [K-Dense](https://k-dense.ai). Works with **Cursor, Claude Code, Codex, Google Antigravity, and more**. Transform your AI agent into a research assistant capable of executing complex multi-step scientific workflows across biology, chemistry, medicine, and beyond.
> **Stay up to date:** Follow K-Dense on [X](https://x.com/k_dense_ai), [LinkedIn](https://www.linkedin.com/company/k-dense-inc), [YouTube](https://www.youtube.com/@K-Dense-Inc), and [Reddit](https://www.reddit.com/user/-k-dense-/) for new skills, release announcements, walkthroughs, research workflow demos, and examples you can use with your own AI agent.
A comprehensive collection of **163 ready-to-use scientific and research skills** (covering cancer genomics, individual-level 1000 Genomes queries, hosted regulatory-sequence prediction, live pathogen-variant surveillance, analytical method validation, PK/PD modelling and dose selection, full-text biomedical and regulatory literature retrieval, drug-target binding, bounded biomedical knowledge graph search, molecular dynamics, RNA velocity, microbiome foundation models, geospatial science, time series forecasting, scientific ML resource discovery via Hugging Science, 78+ scientific databases, and more) for any AI agent that supports the open [Agent Skills](https://agentskills.io/) standard, created by [K-Dense](https://k-dense.ai). The repository is also a portable [Agent Plugins](https://agent-plugins.org/) package (`plugin.json` + `skills/`), so plugin-capable clients can load the whole collection as one plugin. Works with **Cursor, Claude Code, Codex, Google Antigravity, and more**. Transform your AI agent into a research assistant capable of executing complex multi-step scientific workflows across biology, chemistry, medicine, and beyond.
> ⭐ **Help make AI for science easier to discover:** If Scientific Agent Skills saves you time, teaches your agent a workflow, or helps your lab move faster, please [star this repository](https://github.com/K-Dense-AI/scientific-agent-skills). A star is a public signal that these open, reusable research skills are worth maintaining: it helps scientists, engineers, and open-source contributors find the project, shows which agent-skill standards are gaining real adoption, and gives us a clear reason to keep expanding the collection for the community.
@@ -72,13 +67,25 @@ These skills enable your AI agent to seamlessly work with specialized scientific
> 🎬 **New to Scientific Agent Skills?** Watch our [Getting Started with Scientific Agent Skills](https://youtu.be/ZxbnDaD_FVg) video for a quick walkthrough.
### 🎥 More tutorials
Recorded walkthroughs of these skills on real research tasks, from the [K-Dense YouTube channel](https://www.youtube.com/@K-Dense-Inc):
| Video | What it covers |
|-------|----------------|
| [Skills 101: Build Your Own Scientific Agent Skill](https://youtu.be/lVZbHiwzMEg) | Writing, testing, and packaging a new skill from scratch |
| [Literature Review and Hypothesis Generation](https://youtu.be/wKJp8y4ZyiM) | Searching the literature and generating grounded hypotheses |
| [Draft and Budget an Experimental Protocol](https://youtu.be/Yz2L5s_M_34) | Turning a planned experiment into a costed, written protocol |
| [Draft Responses to Reviewer Comments](https://youtu.be/0MmU-Pmtg1o) | Building a point-by-point rebuttal from reviewer feedback |
| [Can AI Reproduce a Nature Medicine Paper?](https://youtu.be/4WTCK9kSfdk) | An end-to-end reproduction attempt on a published analysis |
---
## 📦 What's Included
This repository provides **159 scientific and research skills** organized into the following categories:
This repository provides **163 scientific and research skills** organized into the following categories:
- **100+ Scientific & Financial Databases** - A unified database-lookup skill provides deterministic, provenance-rich access to 78 public databases (PubChem, ChEMBL, UniProt, COSMIC, ClinicalTrials.gov, FRED, USPTO, and more), plus dedicated skills for DepMap, Imaging Data Commons, PrimeKG, U.S. Treasury Fiscal Data, Hugging Science, OneKGPd, and Genomic Intelligence. Multi-database packages like BioServices (~40 bioinformatics services), BioPython (39 NCBI sub-databases via Entrez), and gget (20+ genomics databases) add further coverage
- **100+ Scientific & Financial Databases** - A unified database-lookup skill provides deterministic, provenance-rich access to 78 public databases (PubChem, ChEMBL, UniProt, COSMIC, ClinicalTrials.gov, FRED, USPTO, and more), plus dedicated skills for DepMap, Imaging Data Commons, PrimeKG, NCATS ARAX, U.S. Treasury Fiscal Data, Hugging Science, OneKGPd, and Genomic Intelligence. Multi-database packages like BioServices (~40 bioinformatics services), BioPython (39 NCBI sub-databases via Entrez), and gget (20+ genomics databases) add further coverage
- **70+ Optimized Python Package Skills** - Explicitly defined, version-aware workflows for RDKit, Scanpy, PyTorch Lightning, scikit-learn, PyTDC, PathML, pydicom, NeuroKit2, PufferLib, QuTiP, GeoPandas, pymatgen, BioPython, Qiskit, Molecular Dynamics (OpenMM/MDAnalysis), and others. The agent can still use *any* Python package; these skills provide stronger, safer guidance for the packages listed
- **9 Scientific Integration Skills** - Explicitly defined skills for Benchling, DNAnexus, LatchBio, OMERO, Protocols.io, Open Notebook, Ginkgo Cloud Lab, LabArchives, and Opentrons. Again, the agent is not limited to these — any API or platform reachable from Python is fair game; these skills are the optimized, pre-documented paths
- **30+ Analysis & Communication Tools** - Literature review, evidence-traceable scientific writing, confidential peer review, document processing, Paperclip (full-text papers, FDA/PMDA/EMA filings, and trial registries with line-pinned citations), Paperzilla, Exa Search, macro-free PPTX posters, slides, schematics, infographics, Mermaid diagrams, and more
@@ -123,7 +130,7 @@ Each skill includes:
- **Multi-Step Workflows** - Execute complex pipelines with a single prompt
### 🎯 **Comprehensive Coverage**
- **159 Skills** - Extensive coverage across all major scientific domains
- **161 Skills** - Extensive coverage across all major scientific domains
- **100+ Databases** - Unified access to 78+ databases via database-lookup, plus dedicated data access skills and multi-database packages like BioServices, BioPython, and gget
- **70+ Optimized Python Package Skills** - Current, version-scoped guidance for packages including RDKit, Scanpy, PyTorch Lightning, scikit-learn, PyTDC, pydicom, PufferLib, QuTiP, GeoPandas, pymatgen, Qiskit, Molecular Dynamics (OpenMM/MDAnalysis), scVelo, and TimesFM (the agent can use any Python package; these are the pre-documented paths)
@@ -178,7 +185,7 @@ Pin to a specific release tag or commit SHA for reproducible installs:
```bash
# Pin to a release tag
gh skill install K-Dense-AI/scientific-agent-skills --pin v2.62.0
gh skill install K-Dense-AI/scientific-agent-skills --pin v2.64.0
# Pin to a commit SHA
gh skill install K-Dense-AI/scientific-agent-skills --pin abc123def
@@ -194,6 +201,27 @@ gh skill update
gh skill update --all
```
### Option 3: Agent Plugins (Cursor, Codex, and other plugin clients)
This repository is a valid [Agent Plugins](https://agent-plugins.org/) 1.0.0 package: root [`plugin.json`](plugin.json) plus Agent Skills under `skills/`. Clients that support the standard discover every immediate child of `skills/` that contains a `SKILL.md`.
**Cursor** — symlink or copy the repo into the local plugins directory, then reload:
```bash
mkdir -p ~/.cursor/plugins/local
ln -s "$(pwd)" ~/.cursor/plugins/local/scientific-agent-skills
```
Restart Cursor or run **Developer: Reload Window**, then confirm the plugin and its skills appear under **Customize**. See [Cursor plugins](https://cursor.com/docs/plugins).
**Codex** — install from a local checkout (confirm the current CLI flag names in Codex docs):
```bash
codex plugins install .
```
Compatible clients (Cursor, Codex, GitHub Copilot, VS Code, Kiro, and others listed at [agent-plugins.org](https://agent-plugins.org/compatible-clients)) share the same package layout; installation UX stays client-specific.
### Other Agent Skills hosts (OpenClaw, NemoClaw, Pi, Hermes, …)
Agent hosts differ in install paths, discovery settings, and support for optional frontmatter fields. `npx skills add` (Option 1) commonly installs into the `~/.agents/skills/` convention, with project-scoped installs under `.agents/skills/`; confirm both paths against your host's current documentation. To install manually on a host configured to scan one of those locations:
@@ -209,7 +237,7 @@ For Hermes versions that support skill taps, add the repository as a tap:
hermes skills tap add K-Dense-AI/scientific-agent-skills
```
Every `SKILL.md` has YAML frontmatter, but legacy and community skills vary in `metadata` formatting (block or flow style) and optional extension fields. Repository updates must keep `metadata.version` as a quoted numeric string and pass canonical `skills-ref validate ./skills/<skill-name>` checks. Hosts may interpret optional metadata and credential prompts differently, so verify behavior on the target host. Because 159 skills add up to a lot of standing context, consider installing a topical subset rather than the whole collection.
Every `SKILL.md` has YAML frontmatter, but legacy and community skills vary in `metadata` formatting (block or flow style) and optional extension fields. Repository updates must keep `metadata.version` as a quoted numeric string and pass canonical `skills-ref validate ./skills/<skill-name>` checks. Hosts may interpret optional metadata and credential prompts differently, so verify behavior on the target host. Because 161 skills add up to a lot of standing context, consider installing a topical subset rather than the whole collection.
> **NemoClaw note:** NemoClaw runs agents inside NVIDIA OpenShell with default-deny outbound networking. Skills are discovered and loaded normally, but any skill that needs the network — package installs via `uv`, or API calls (Exa, Parallel, Benchling, NCBI, Materials Project, …) — only works once the operator pre-approves the relevant domains in the OpenShell TUI.
@@ -448,13 +476,13 @@ networks, and search GEO for similar patterns.
## 📚 Available Skills
This repository contains **159 scientific and research skills** organized across multiple domains. Each skill provides comprehensive documentation, code examples, and best practices for working with scientific libraries, databases, and tools.
This repository contains **163 scientific and research skills** organized across multiple domains. Each skill provides comprehensive documentation, code examples, and best practices for working with scientific libraries, databases, and tools.
### Skill Categories
> **Note:** The Python package and integration skills listed below are *explicitly defined* skills — curated with documentation, examples, and best practices for stronger, more reliable performance. They are not a ceiling: the agent can install and use *any* Python package or call *any* API, even without a dedicated skill. The skills listed simply make common workflows faster and more dependable.
#### 🧬 **Bioinformatics & Genomics** (26 skills)
#### 🧬 **Bioinformatics & Genomics** (27 skills)
- RNA-seq pipelines: Bulk RNA-seq (end-to-end FASTQ -> counts -> DE -> enrichment orchestrator)
- Sequence analysis: BioPython, pysam, scikit-bio, BioServices
- Single-cell analysis: Scanpy, AnnData, scvi-tools, scVelo (RNA velocity), Arboreto, Cellxgene Census
@@ -464,6 +492,7 @@ This repository contains **159 scientific and research skills** organized across
- Differential expression: PyDESeq2
- Functional enrichment: Pathway Enrichment (ORA, GSEA/preranked, ssGSEA via gseapy + g:Profiler; GO, KEGG, Reactome, WikiPathways, MSigDB)
- Phylogenetics: ETE Toolkit, Phylogenetics (MAFFT, IQ-TREE 2, FastTree)
- Microbiome foundation models: Waypoint (Outpost Bio's open Waypoint-6m/45m/170m checkpoints, the Atlas 539k-sample MGnify pretraining corpus, and the eight-task Compass benchmark — embedding, fine-tuning, benchmarking, and pretraining on taxonomic abundance profiles, with MetaPhlAn/Kraken2/QIIME 2 conversion)
#### 🧪 **Cheminformatics & Drug Discovery** (10 skills)
- Molecular manipulation: RDKit, Datamol, Molfeat
@@ -512,7 +541,8 @@ This repository contains **159 scientific and research skills** organized across
- Astronomy: Astropy
- Quantum computing: Cirq, PennyLane, Qiskit, QuTiP 5.3
#### ⚙️ **Engineering & Simulation** (5 skills)
#### ⚙️ **Engineering & Simulation** (6 skills)
- Lab hardware CAD: parametric build123d 0.11.1 models for microfluidic chips and molds, optomechanical mounts, microplate and cuvette adapters, and behavior rigs, checked against ANSI/SLAS and optical-table dimensional standards and reviewed with mandatory multi-view renders
- Numerical computing: proprietary MATLAB R2026a and distinct GNU Octave 11.3 planning/review workflows
- Computational fluid dynamics: bounded FluidSim 0.9 simulations with numerical-validity and HPC checks
- Experimental flow measurement: OpenPIV (velocity fields from PIV image pairs, interrogation-window cross-correlation, spurious-vector validation, vorticity/strain-rate/turbulence statistics)
@@ -564,12 +594,13 @@ This repository contains **159 scientific and research skills** organized across
- Citations: Citation Management, pyzotero
- Illustration: Generate Image (AI image generation with FLUX.2 Pro and Gemini 3.1 Flash Image / Nano Banana 2)
#### 🔬 **Scientific Databases & Data Access** (10 skills → 100+ databases total)
#### 🔬 **Scientific Databases & Data Access** (11 skills → 100+ databases total)
> A unified database-lookup skill provides deterministic REST API access to 78 public databases across all domains, with retrieval contracts, pagination/count reconciliation, and endpoint provenance. Dedicated skills cover specialized data platforms. Multi-database packages like BioServices (~40 bioinformatics services), BioPython (39 NCBI sub-databases via Entrez), and gget (20+ genomics databases) add further coverage.
- Unified access: Database Lookup (78 databases spanning chemistry, genomics, clinical, pathways, patents, economics, and more — PubChem, ChEMBL, UniProt, PDB, AlphaFold, KEGG, Reactome, STRING, ClinVar, COSMIC, ClinicalTrials.gov, FDA, FRED, USPTO, SEC EDGAR, and dozens more — with auditable filters and provenance)
- Cancer genomics: DepMap (cancer cell line dependencies, drug sensitivity, gene effect profiles)
- Cancer imaging: Imaging Data Commons (NCI radiology & pathology datasets via idc-index)
- Knowledge graph: PrimeKG (precision medicine knowledge graph — genes, drugs, diseases, phenotypes)
- Biomedical knowledge graph search: [NCATS ARAX](skills/ncats-arax/) (bounded, Biolink-constrained one-hop and endpoint-pinned two-hop queries over knowledge graphs with up to five explicitly selected NCATS Translator providers, with provenance preservation)
- Fiscal data: U.S. Treasury Fiscal Data (national debt, Treasury statements, auctions, exchange rates)
- Scientific ML resource catalog: Hugging Science (curated index of datasets, models, blog posts, and interactive Spaces across 17 scientific domains — astronomy, biology, chemistry, climate, genomics, materials science, medicine, physics, scientific reasoning, and more — with usage patterns for `datasets`, `transformers`, and `gradio_client`)
- Individual-level population genomics: OneKGPd (3,202-person high-coverage 1000 Genomes cohort queries)
@@ -620,9 +651,13 @@ Deep dives, benchmarks, and guides from the [K-Dense blog](https://www.k-dense.a
- **[Agent Skills: The Final Piece for AI-Powered Scientific Research](https://www.k-dense.ai/blog/agent-skills-final-piece-for-ai-powered-research)** — What Agent Skills are, why curated domain guidance beats raw model capability, and an introduction to this repository.
- **[K-Dense Web vs Scientific Agent Skills: Why We Built Both (And Which One You Should Use)](https://www.k-dense.ai/blog/k-dense-web-vs-scientific-agent-skills)** — When the open-source skills are the right tool, and when a hosted platform with managed compute makes more sense.
- **[AI Co-Scientists, Answered: 20 Questions from a Live Session with a University Research Center](https://www.k-dense.ai/blog/ai-co-scientists-answered-20-questions)** — Practical questions from a research center evaluating AI co-scientists: what stays open source and MIT-licensed, how local and desktop deployments work, how data is handled, and how to choose between the hosted platform and the BYOK setup that runs these skills.
- **[How to Use Multica for Scientific Research](https://www.k-dense.ai/blog/multica-scientific-research)** — A self-hosted Multica workspace plus a curated subset of these skills: clinical-trial and variant analyses, literature review, weekly autopilots, and a second-model audit, with each skill imported from `skills/<name>/`.
### Skill benchmarks and deep dives
- **[The Silent 97%: Introducing the waypoint-bio Agent Skill](https://www.k-dense.ai/blog/introducing-waypoint-agent-skill)** — [waypoint-bio](skills/waypoint-bio/) against silent data loss: an unconverted MetaPhlAn table keeps 3% of abundance mass and still returns a valid embedding; skill-equipped agents won 16 to 0 on matched pairs.
- **[The Millimetre Problem: Introducing the lab-hardware-cad Agent Skill](https://www.k-dense.ai/blog/lab-hardware-cad-skill)** — [lab-hardware-cad](skills/lab-hardware-cad/) over 98 geometry-scored runs: the skill arm produced parametric, regenerable models in 49 of 49 cases (baseline 0 of 49) and named the missing Y-maze standard instead of inventing one.
- **[One Skill, 78 Databases: Why We Didn't Build 78 Skills](https://www.k-dense.ai/blog/database-lookup-one-skill-78-databases)** — The design rationale behind [database-lookup](skills/database-lookup/): consolidation cut always-on context cost by 13.9x while holding routing accuracy across five models.
- **[Can an AI Agent Run Your Mass Spec Pipeline? Benchmarking the PyOpenMS Skill](https://www.k-dense.ai/blog/benchmarking-pyopenms-skill-mass-spectrometry)** — A 250-run study of [pyopenms](skills/pyopenms/): 100% task success with the skill versus 96% without, 92% fewer pyOpenMS API errors, and 10% lower cost.
- **[Beyond RDKit: Benchmarking the Rowan Agent Skill Against Experiment](https://www.k-dense.ai/blog/benchmarking-rowan-skill-chemistry)** — [rowan](skills/rowan/) compared against RDKit and experimental data: pKa MAE 0.23 (R² 0.986), logD₇.₄ MAE 1.15, and 0.19 Å RMSD docking pose recovery for roughly $0.52 of compute.
@@ -631,6 +666,13 @@ Deep dives, benchmarks, and guides from the [K-Dense blog](https://www.k-dense.a
- **[Benchmarking Nano Banana 2 Lite for Scientific Image Generation](https://www.k-dense.ai/blog/benchmarking-nano-banana-2-lite-scientific-image-model)** — A 240-image comparison of scientific-diagram models, useful when choosing a backend for [generate-image](skills/generate-image/): 3.8 s median latency for Nano Banana 2 Lite against 49 s for GPT Image 2, with a quality tradeoff.
- **[Benchmarking NVIDIA BioNeMo Agent Toolkit Skills for NIM microservices](https://www.k-dense.ai/blog/benchmarking-nvidia-bionemo-nim-skill)** — A separate NVIDIA skill set rather than one of these, but the findings generalize: skills help most with routing to non-obvious endpoints and with weak-model reliability, and do not improve the underlying scientific model's accuracy.
### Why the workflow layer matters
- **[The Model Is No Longer the Bottleneck](https://www.k-dense.ai/blog/the-model-is-no-longer-the-bottleneck)** — The case for why a repository like this one exists: frontier models now match specialized scientific software on raw capability (±0.079 ppm on NMR hydrogen shift prediction), so the limiting factor has moved to the workflow around the model — data access, code execution, verification, and auditable output.
- **[The AI Co-Scientist Is Here. The Bottleneck Is Verification.](https://www.k-dense.ai/blog/ai-co-scientist-verification-bottleneck)** — A 10-point checklist for evaluating a research agent, built around exposing sources, code, data provenance, and intermediate work rather than a polished final answer — the same reasoning behind the provenance and retrieval-contract requirements in skills like [database-lookup](skills/database-lookup/) and [scientific-writing](skills/scientific-writing/).
- **[Reproduction, Not Generation, Is AI's Killer App for Science](https://www.k-dense.ai/blog/reproduction-not-generation-ai-for-science)** — Why re-running published analyses is the highest-value use of an agent: 78% of papers and 93% of individual analysis tasks reproduced across a 221-study benchmark, because a reproduction can be checked against known numbers while a generated claim cannot.
- **[Introducing K-Bench 01: Nine Frontier Models, 178 Real Scientific Tasks, and a Lot of Confident Wrong Answers](https://www.k-dense.ai/blog/introducing-k-bench-01-internal-benchmark)** — Nine frontier models on 178 real user tasks, with overclaiming in 40% of runs. Useful calibration for what to check when an agent reports success, and context for the verification boundaries written into the clinical, regulatory, and research-methodology skills above.
### Security and safe deployment
- **[Security in the Science Agent Era: What Every Lab Needs to Know Before Installing Skills](https://www.k-dense.ai/blog/skill-security-before-you-install)** — The practical review checklist behind this repo's [Security Disclaimer](#%EF%B8%8F-security-disclaimer): read the full `SKILL.md` and `scripts/`, scan before installing, and pin versions instead of tracking a branch.
@@ -641,6 +683,8 @@ Deep dives, benchmarks, and guides from the [K-Dense blog](https://www.k-dense.a
- **[Introducing Science Superpowers: Scientific Discipline for Your Research Agent](https://www.k-dense.ai/blog/introducing-science-superpowers)** — Hypothesis pre-registration, reproducible workflows, and verification-before-claims that wrap around these skills to guard against p-hacking and HARKing.
- **[Your AI Assistant Reasons Like a Generalist. Science Needs a Specialist.](https://www.k-dense.ai/blog/introducing-scientific-agents)** — 503 open-source `AGENTS.md` profiles supplying the "how to think" layer alongside the "what to do" procedures in these skills.
- **[Introducing mimeo and 80+ Mimeographs](https://www.k-dense.ai/blog/introducing-mimeo-and-mimeographs)** — Generate your own `SKILL.md` / `AGENTS.md` expert profiles by distilling how a given practitioner reasons.
- **[Agentic Data Scientist: An Open Source AI That Actually Does the Analysis](https://www.k-dense.ai/blog/agentic-data-scientist-open-source)** — A multi-agent planning, execution, and validation harness that loads these skills for end-to-end data-science workflows.
- **[Karpathy: An Open Source Agentic Machine Learning Engineer](https://www.k-dense.ai/blog/karpathy-agentic-ml-engineer)** — An autonomous ML-training agent built to consume Scientific Agent Skills for preprocessing through hyperparameter search.
---
@@ -817,7 +861,7 @@ Need help? Here's how to get support:
- 📖 **Documentation**: Check the relevant `SKILL.md` and `references/` folders
- 🐛 **Bug Reports**: [Open an issue](https://github.com/K-Dense-AI/scientific-agent-skills/issues)
- 💡 **Feature Requests**: [Submit a feature request](https://github.com/K-Dense-AI/scientific-agent-skills/issues/new)
- 📣 **Updates and demos**: Follow [X](https://x.com/k_dense_ai), [LinkedIn](https://www.linkedin.com/company/k-dense-inc), and [YouTube](https://www.youtube.com/@K-Dense-Inc) to keep up with new skills, tutorials, and Scientific Agent Skills releases
- 📣 **Updates and demos**: Follow [X](https://x.com/k_dense_ai), [LinkedIn](https://www.linkedin.com/company/k-dense-inc), [YouTube](https://www.youtube.com/@K-Dense-Inc), and [Reddit](https://www.reddit.com/user/-k-dense-/) to keep up with new skills, tutorials, and Scientific Agent Skills releases
- 💼 **Enterprise Support**: Contact [K-Dense](https://k-dense.ai/) for commercial support
---
@@ -842,7 +886,7 @@ Recommended practice:
title = {Scientific Agent Skills: A Comprehensive Collection of Scientific Tools for AI Agents},
year = {2026},
url = {https://github.com/K-Dense-AI/scientific-agent-skills},
note = {159 skills covering databases, packages, integrations, and analysis tools}
note = {161 skills covering databases, packages, integrations, and analysis tools}
}
```
@@ -905,3 +949,13 @@ See [LICENSE.md](LICENSE.md) for full terms.
### Individual Skill Licenses
> ⚠️ **Important**: Each skill has its own license specified in the `license` metadata field within its `SKILL.md` file. These licenses may differ from the repository's MIT License and may include additional terms or restrictions. **Users are responsible for reviewing and adhering to the license terms of each individual skill they use.**
## Star History
<a href="https://star-history.dera.page/#K-Dense-AI/scientific-agent-skills">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://star-history.dera.page/svg?repos=K-Dense-AI/scientific-agent-skills&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://star-history.dera.page/svg?repos=K-Dense-AI/scientific-agent-skills" />
<img alt="Star History Chart" src="https://star-history.dera.page/svg?repos=K-Dense-AI/scientific-agent-skills" />
</picture>
</a>
@@ -2,9 +2,9 @@
title: "Security Report"
task: ""
lineage_type: import
upstream_source: https://github.com/K-Dense-AI/scientific-agent-skills/blob/991bd993/docs/security-report.json
upstream_sha: 991bd993
imported_at: 2026-08-08
upstream_source: https://github.com/K-Dense-AI/scientific-agent-skills/blob/28f5603b/docs/security-report.json
upstream_sha: 28f5603b
imported_at: 2026-08-17
prompt_class: catalogue
upstream_changes: accepted
author: upstream
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,378 @@
---
lineage_type: import
upstream_source: https://github.com/K-Dense-AI/scientific-agent-skills/blob/336c4f83/skills/lab-hardware-cad/SKILL.md
upstream_sha: 336c4f83
imported_at: 2026-08-16
prompt_class: catalogue
upstream_changes: accepted
name: lab-hardware-cad
description: Design custom laboratory hardware as parametric build123d models and export fabrication-ready STEP, STL, and DXF files - microfluidic chips and molds, optomechanical mounts and breadboard adapters, cuvette and microplate holders, tube racks, animal-behavior rigs, and 3D-printed instrument fixtures. Use when a research task needs a physical part that must mate with standardized labware, an optical table, a cage system, or a printer, CNC, or laser process.
license: MIT
compatibility: Python 3.10-3.14 with build123d 0.11.1 and matplotlib for snapshots. Geometry commands require build123d; the standards lookup and the interface check run on the standard library alone. No network access needed.
allowed-tools: Read Write Edit Bash Glob Grep
metadata:
version: "1.2"
skill-author: K-Dense Inc.
last-reviewed: "2026-08-15"
build123d-version: "0.11.1"
---
# Lab Hardware CAD
Design physical research hardware as **parametric Python source**, export STEP as the
authoritative artifact, and verify the result both numerically and visually before anything
is fabricated.
The hard part of lab hardware is almost never the geometry. It is that the part must mate with
equipment whose dimensions are fixed by a published standard or a vendor drawing. A holder that
is 0.5 mm too wide does not fit the plate reader; a channel with the wrong aspect ratio collapses
during bonding; a mount whose bolt pattern is 25.4 mm instead of 25.0 mm will not reach the
optical table. This skill exists to keep those numbers correct and checked.
## When to use
Use for any request to design, model, or fabricate a physical part for a lab: chip, mold, mount,
adapter, holder, rack, bracket, enclosure, jig, fixture, arena, or maze. Also use to inspect or
modify an existing STEP file.
Do **not** use for finite-element analysis, computational fluid dynamics, molecular structure,
or scientific plotting. Those are different skills.
## Setup
```bash
uv venv --python 3.12 .venv-labcad
uv pip install --python .venv-labcad/bin/python "build123d==0.11.1" "matplotlib>=3.8"
```
build123d 0.11.1 requires Python >=3.10,<3.15 and pulls in the OpenCascade kernel through
`cadquery-ocp-novtk`. The wheel is large; install once per project and reuse it.
All bundled scripts take `--help`. `check.py standards` runs without build123d installed.
**Model files are executed, not parsed.** `gen.py`, `check.py`, and `snapshot.py` import a
`*_model.py` and call its `build()`, which runs arbitrary Python in the current environment. That
is inherent to parametric CAD — the source is the design. Only run model files authored in this
session or supplied by the user from a trusted location. If a model came from the internet, a
shared drive, or an untrusted colleague, read it before running it and say that you did.
## Required workflow
Follow these steps in order. Steps 5 and 6 are not optional, and step 6 is not waived by step 5
passing.
### 1. Route to a device family
Read the request, classify it, and load **exactly one** family reference. Do not load all four —
they are long, and mixing conventions between families is a common source of error.
| If the part is | Load |
| --- | --- |
| A chip, mold, channel network, flow cell, gasket, or anything with fluid ports | `references/microfluidics.md` |
| A mount, post, breadboard adapter, cage-system part, filter or sample holder in a beam path | `references/optomechanics.md` |
| An adapter, insert, rack, or holder for plates, cuvettes, tubes, slides, or dishes | `references/labware-adapters.md` |
| An arena, maze, head-fixation part, spout, tether, or extrusion-mounted enclosure for animal work | `references/behavior-rigs.md` |
If the part genuinely spans two families — a microfluidic chip that bolts to an optical table —
load the family that owns the **critical interface**, then read only the interface section of the
second. State in your response which family you routed to.
### 2. Establish the interface dimensions before any geometry
Every part has at least one mating interface. Before writing code, write down for each interface:
- the **source** of the dimension: a published standard, a vendor drawing, or a user measurement;
- the **nominal value and tolerance**;
- the **clearance or interference** you intend, and why.
Look the number up in `assets/standards.json` or the family reference. **Never write an interface
dimension from memory.** If the number is not in the standards file or the reference, ask the user
for the vendor drawing or the measurement rather than guessing. A guessed interface dimension is
the single most expensive failure mode in this skill.
A feature that must *receive* a standardised component is sized against that component's
**maximum material condition** — nominal plus its plus-tolerance — and only then given clearance.
Sized from nominal instead, it fits only the smaller half of conforming parts.
```bash
python scripts/check.py standards --list
python scripts/check.py standards --show slas-microplate-footprint
```
The bundled standard IDs (exact strings; do not guess variants): `slas-microplate-footprint`,
`slas-microplate-height`, `slas-microplate-flange`, `slas-well-positions-96`,
`slas-well-positions-384`, `slas-well-positions-1536`, `cuvette-standard-10mm`,
`optical-breadboard-metric`, `optical-breadboard-imperial`, `cage-system-30mm`,
`sm1-lens-tube-thread`.
If the part mates with nothing in this list, that is common and fine: declare no interfaces,
and name every interface dimension with its source (user spec, vendor drawing, measurement) as
**unchecked** in the report. Never declare against an unrelated standard to fill the gap — a
fabricated declaration is worse than an honest "nobody checked this".
### 3. Choose the process before choosing the geometry
Read `references/fabrication-limits.md`. Process determines minimum wall, minimum feature,
achievable tolerance, and whether the part survives autoclaving or contact with your solvent.
FDM cannot hold ±0.05 mm; SLA resin is generally not safe for cell contact without post-cure and
testing. Record the process and material in the model docstring.
### 4. Author a parametric model
Write `<part>_model.py`. The source is the authoritative artifact — **never hand-edit an exported
STEP file**, and never regenerate from a mesh.
Requirements:
- Every dimension that a user might change is a **module-level named constant** with units in the
name: `bore_d_mm`, `wall_t_mm`, `post_h_mm`. No bare numbers in the body except 0, 1, and 2.
- Expose `build() -> Part`. `gen.py` calls it.
- Group parameters into an `INTERFACE` block (dimensions fixed by a standard, annotated with the
standard ID) and a `DESIGN` block (dimensions you are free to choose).
- **Derive every computed dimension inside a function**, never at module level, so `--param`
overrides actually reach it.
- Declare an `interfaces()` function returning the dimensions the part must fit, each with its
standard ID and intent. This is what makes the interface machine-checkable in step 5.
`intent` is `"envelope"` when the feature must **accept** any conforming part (a pocket, bore,
or slot — checked one-sided at maximum material condition plus your clearance) and `"match"`
when this part must itself conform (symmetric band). `clearance` is the total intended
clearance in mm and must be non-negative. Declare only dimensions that constrain *this part's
mating features* — a property of the mating equipment (a table's edge border, a typical plate
thickness) is not an interface of yours. If no bundled standard applies, return `[]`.
- Declare a `checks()` function of **go/no-go gauges measured from the built solid**: a `clear`
region for everything that must pass through or fit in (screw shafts, beam corridors, the
mating part at maximum material condition dropping into its pocket), a `material` region for
everything that must remain (a ridge, a ledge, a screw seat), and a `bbox_*` bound for every
size limit the user stated. Map **every geometric requirement in the request** to one entry;
these catch the errors that `is_valid`, the bounding box, and declared numbers cannot see.
`gen.py` runs them on every generation and fails the build when one fails. Schema and worked
examples: `references/build123d-patterns.md`.
- Put the process, material, and every interface source in the module docstring.
```python
"""SLAS microplate carrier for a custom stage insert.
Process: FDM, PETG, 0.2 mm layer. Tolerance budget +/-0.3 mm.
Interfaces:
- Plate pocket: ANSI/SLAS 1-2004 (R2012) footprint 127.76 x 85.48 mm, +/-0.25.
- Stage bolts: user-measured, 40.0 mm centres (drawing in docs/stage.pdf).
"""
from build123d import *
# --- INTERFACE (fixed by standard; do not tune) ---
plate_l_mm = 127.76 # ANSI/SLAS 1-2004 nominal
plate_w_mm = 85.48 # ANSI/SLAS 1-2004 nominal
plate_tol_mm = 0.25 # ANSI/SLAS 1-2004; the pocket is sized to nominal + this
# --- DESIGN (free) ---
pocket_clearance_mm = 0.40 # per-side; FDM, see fabrication-limits.md
wall_t_mm = 3.0
floor_t_mm = 2.5
body_h_mm = 12.0
def pocket_mm() -> tuple[float, float]:
"""Pocket at the plate's maximum material condition plus clearance per side.
A pocket sized from nominal jams on roughly half of conforming plates.
"""
growth = plate_tol_mm + 2 * pocket_clearance_mm
return plate_l_mm + growth, plate_w_mm + growth
def interfaces() -> list[dict]:
"""What this part must fit. `check.py interfaces` verifies every entry."""
pocket_l, pocket_w = pocket_mm()
return [
{"feature": "plate pocket length", "standard": "slas-microplate-footprint",
"dimension": "footprint_length", "value": pocket_l,
"intent": "envelope", "clearance": 2 * pocket_clearance_mm},
{"feature": "plate pocket width", "standard": "slas-microplate-footprint",
"dimension": "footprint_width", "value": pocket_w,
"intent": "envelope", "clearance": 2 * pocket_clearance_mm},
]
def checks() -> list[dict]:
"""Gauges measured from the built solid. Sized from the REQUIREMENT's numbers
(plate MMC, the user's height limit), not from the pocket parameters, so a
wrong parameter cannot shrink the gauge to match the wrong geometry."""
depth = body_h_mm - floor_t_mm
return [
{"feature": "plate at MMC drops into the pocket",
"clear": {"box": (plate_l_mm + plate_tol_mm, plate_w_mm + plate_tol_mm, depth),
"at": [(0.0, 0.0, floor_t_mm + depth / 2)]}},
{"feature": "under 15 mm for the stage", "bbox_z": {"max": 15.0}},
]
def build() -> Part:
pocket_l, pocket_w = pocket_mm()
with BuildPart() as carrier:
Box(pocket_l + 2 * wall_t_mm, pocket_w + 2 * wall_t_mm, body_h_mm,
align=(Align.CENTER, Align.CENTER, Align.MIN))
with Locations((0, 0, floor_t_mm)):
Box(pocket_l, pocket_w, body_h_mm, mode=Mode.SUBTRACT,
align=(Align.CENTER, Align.CENTER, Align.MIN))
return carrier.part
```
See `references/build123d-patterns.md` for the builder-vs-algebra choice, the `interfaces()`
contract, sketching, selectors, fillets, and threaded-insert bores.
### 5. Generate and run the checks
```bash
python scripts/gen.py carrier_model.py --outdir out/
python scripts/check.py facts out/carrier.step
python scripts/check.py interfaces out/carrier.manifest.json
python scripts/check.py geometry out/carrier.step --model carrier_model.py
```
`gen.py` also evaluates the model's `checks()` gauges against the solid it just built, prints
each PASS/FAIL, records them in the manifest, and exits non-zero on a failure — so a part that
violates its own declared geometry never silently becomes an artifact. `check.py geometry`
re-runs the same gauges against the exported STEP, which is the authoritative artifact.
`out/` is a scratch convention, not a requirement. When the user asked for deliverables in a
specific place, generate there (`--outdir .`) or copy the STEP, manifest, and DXF to it before
finishing — a deliverable that exists only inside `out/` has not been delivered.
`gen.py` writes `carrier.step` (authoritative), `carrier.stl` (mesh preview and printing), and
`carrier.manifest.json` recording the source hash, resolved parameters, declared interfaces,
library versions, and measured bounding box, volume, and validity. The manifest is the provenance
record — keep it with the artifact.
`check.py facts` reports `is_valid`, bounding box, volume, surface area, centre of mass, and
solid count. A part that reports `is_valid: false` is broken geometry; fix the source before going
further.
`check.py interfaces` evaluates every entry the model declared against the standards database
and exits non-zero on failure. **Be clear about what it does and does not verify:** it checks the
*declared numbers* — catching a transcribed dimension, the wrong standard, and
nominal-instead-of-MMC sizing — but it never measures the built geometry, and a value computed
from the same constants it is checked against passes with zero headroom by construction. Do not
cite it as evidence the geometry is right; `facts` and the snapshot are the geometry checks.
An empty declaration list passes: a part that mates with nothing in the bundled database has
nothing to declare, and its interface dimensions are instead named as unchecked in the report.
Use `interfaces` rather than `check.py fit` for anything internal — a pocket, bore, or slot does
not appear in the part's outer bounding box, which is what `fit` measures. Reach for `fit` only
to check one number by hand (`--value footprint_length=128.81`), or when the part's own outline
is the interface, such as a gasket cut to a plate footprint.
For assemblies, check that parts do not interfere:
```bash
python scripts/check.py clearance out/carrier.step out/lid.step --min 0.3
```
### 6. Snapshot and actually look at it
```bash
python scripts/snapshot.py out/carrier.step --out out/carrier.png
```
Then **read the PNG**. This step is mandatory after every generation and every modification.
Deterministic checks passing is not a reason to skip it: `is_valid` and a correct bounding box are
both fully consistent with a pocket cut on the wrong face, a boss placed outside the body, or a
fillet that ate a feature. Those errors are obvious in a picture and invisible in the numbers.
Know the render's limits too. A feature much smaller than the frame — a 0.3 mm mold ridge on a
40 mm part, a counterbore step on a plate — may not be decidable from the views at all. Do not
report seeing something the image cannot resolve; that is worse than not looking. For such
features the skill has instruments: `check.py bores` prints every cylindrical face (diameter,
axis, position, span, sweep) so you can reconcile the drilling against the model's intent, and
`check.py probe` answers a one-off "is this region clear / is material present here" without
editing the model. Cite the measured numbers; report from the picture only what the picture
actually shows.
The six views are true orthographic projections, and the outlines are the model's real edges drawn
**without hidden-line removal**. So a circle visible "through" material is a bore on the far side,
not a window — the part is not transparent. Read it that way rather than reporting a hole that
is not there.
State in your response what you saw in the snapshot, not merely that you generated one.
### 7. Repair through the source
If any check fails, edit the parameters or the model code, rerun `gen.py`, and rerun **both**
step 5 and step 6. Never patch the STEP.
### 8. Report before fabrication
Work through `references/validation.md` and give the user: the process and material, every
interface dimension with its source and tolerance, the clearances chosen, what the snapshot showed,
and any check that did not pass.
Flag explicitly every interface the automatic check could not cover — a vendor drawing, a user
measurement, a standard not in the bundled database. `check.py interfaces` reports only what the
model declared against a known standard, so silence there is not confirmation; a dimension nobody
could check has to be named as such.
## Units
build123d is unitless internally and everything in this skill is **millimetres and degrees**.
`export_step` is called with `Unit.MM`. Imperial hardware appears throughout optomechanics
(1/4-20 screws, 1 inch grids, SM1 threads); convert to millimetres in a single named constant at
the point of definition and never mix systems inside an expression. 1 inch is exactly 25.4 mm, and
a 25 mm metric optical grid is **not** interchangeable with a 1 inch imperial grid — the error
accumulates to 1.6 mm over four holes.
## Tolerances and fits
A nominal dimension is not a fit. Every mating dimension needs a deliberate clearance chosen from
the process tolerance in `references/fabrication-limits.md`. Common defaults, per side:
| Fit | FDM | SLA | CNC |
| --- | --- | --- | --- |
| Free-sliding (plate in a pocket) | 0.40 mm | 0.20 mm | 0.10 mm |
| Located but removable | 0.25 mm | 0.10 mm | 0.05 mm |
| Press / interference | -0.05 mm | -0.03 mm | -0.02 mm |
These are starting points for a first article, not guarantees. Say so when you report them, and
recommend printing a test coupon of the critical interface before committing to a full part.
## Scientific caveats
- **Material compatibility governs.** A geometrically perfect part in the wrong polymer fails in
service: autoclave cycles distort PLA, many solvents craze acrylic, and uncured SLA resin is
cytotoxic. Check `references/fabrication-limits.md` before recommending a material for anything
contacting cells, tissue, solvents, or heat.
- **Optical parts have non-geometric requirements.** Autofluorescence, surface roughness, and
stray-light scatter are not visible in a STEP file. Black resin is not automatically low-scatter.
- **Vendor labware varies.** The SLAS standards fix the plate footprint but not well geometry,
skirt profile, or lid fit, and consumable tubes differ between suppliers. Design to the standard
where one exists; otherwise require a measurement.
- **A passing bounding box is not a passing part.** `fit` checks the dimensions it is given. It
cannot see a missing feature, and it does not replace the snapshot.
## References
| File | Contents |
| --- | --- |
| `references/microfluidics.md` | Channel cross-sections and aspect ratios, mold vs chip polarity, minimum features by process, port and tubing interfaces, bonding lands, dead volume |
| `references/optomechanics.md` | Breadboard grids and screw clearances, post and pedestal heights, 30 mm cage geometry, SM lens-tube threads, beam height |
| `references/labware-adapters.md` | ANSI/SLAS 1-4 microplate dimensions, cuvettes, tubes, slides, dishes, deck and stage constraints |
| `references/behavior-rigs.md` | Arena and maze geometry, head-fixation interfaces, spouts and ports, T-slot extrusion, cleaning and durability |
| `references/fabrication-limits.md` | Process tolerances, minimum walls and features, clearance and thread inserts, materials, autoclave and solvent and biocompatibility |
| `references/validation.md` | Pre-fabrication checklist and the failure modes each item catches |
| `references/build123d-patterns.md` | build123d 0.11.1 API cookbook: builder vs algebra, sketches, selectors, joints, exports |
## Scripts
| Command | Purpose |
| --- | --- |
| `gen.py <model.py> --outdir DIR` | Run `build()`, export STEP and STL, write the provenance manifest |
| `gen.py <model.py> --dxf [--dxf-z MM]` | Also slice a 2D DXF profile for laser cutting (default plane: mid-height) |
| `check.py facts <step>` | Validity, bounding box, volume, area, centre of mass, solid count |
| `check.py interfaces <manifest\|model.py>` | Check every declared interface number against its standard; non-zero exit on failure |
| `check.py geometry <model.py\|step --model M>` | Evaluate the model's `checks()` gauges against the built solid — measured, not declared |
| `check.py probe <step> --cyl D\|--box X,Y,Z --at ...` | One ad-hoc gauge: is this region clear of material, or filled with it |
| `check.py bores <step>` | Census of every cylindrical face: diameter, axis, position, span, sweep |
| `check.py fit --standard ID --value DIM=MM` | Check one dimension by hand, or a part whose outer envelope is the interface |
| `check.py clearance <a> <b> --min MM` | Minimum distance between two solids; detects interference |
| `check.py standards [--list\|--show ID]` | Browse the bundled standards data (standard library only) |
| `snapshot.py <step> --out PNG` | Six-view orthographic and isometric render for visual review |
All commands accept `--json` for machine-readable output and write progress to stderr.
`check.py standards`, and `check.py interfaces` on a manifest, run without build123d installed.
@@ -0,0 +1,211 @@
---
title: "Standards"
task: ""
lineage_type: import
upstream_source: https://github.com/K-Dense-AI/scientific-agent-skills/blob/336c4f83/skills/lab-hardware-cad/assets/standards.json
upstream_sha: 336c4f83
imported_at: 2026-08-16
prompt_class: unknown
upstream_changes: accepted
author: upstream
validated: false
---
{
"schema_version": "1.0",
"units": "mm",
"note": "Dimensional standards for lab-hardware interfaces. Every entry carries a source. Entries with verified=false were not confirmed against the primary document during authoring and must be checked before use.",
"last_reviewed": "2026-08-15",
"standards": {
"slas-microplate-footprint": {
"title": "Microplate footprint (base outline)",
"authority": "ANSI/SLAS",
"document": "ANSI/SLAS 1-2004 (R2012) Footprint Dimensions",
"url": "https://www.slas.org/SLAS/assets/File/public/standards/ANSI_SLAS_1-2004_FootprintDimensions.pdf",
"verified": true,
"dimensions": {
"footprint_length": {
"nominal": 127.76,
"tol_plus": 0.25,
"tol_minus": 0.25,
"note": "Measured within 12.7 mm of the outside corners. Relaxes to +/-0.5 mm at any point along the side."
},
"footprint_width": {
"nominal": 85.48,
"tol_plus": 0.25,
"tol_minus": 0.25,
"note": "Measured within 12.7 mm of the outside corners. Relaxes to +/-0.5 mm at any point along the side."
},
"corner_radius": {
"nominal": 3.18,
"tol_plus": 1.6,
"tol_minus": 1.6,
"note": "Outside radius of the four bottom-flange corners (convex). A receiving pocket's internal fillet must be no LARGER than the minimum (1.58) or it bulges into the plate corner and binds; a sharp pocket corner or a corner-relief cut always clears. Do not size the pocket fillet to the maximum radius."
}
},
"fit_checks": [
{"measure": "bbox_x", "dimension": "footprint_length"},
{"measure": "bbox_y", "dimension": "footprint_width"}
],
"design_note": "For a pocket that receives a plate, add clearance per side on top of the maximum material condition (127.76 + 0.25 = 128.01). Check the pocket, not the plate."
},
"slas-microplate-height": {
"title": "Microplate height",
"authority": "ANSI/SLAS",
"document": "ANSI/SLAS 2-2004 (R2012) Height Dimensions",
"url": "https://www.slas.org/SLAS/assets/File/public/standards/ANSI_SLAS_2-2004_HeightDimensions.pdf",
"verified": true,
"dimensions": {
"plate_height": {
"nominal": 14.35,
"tol_plus": 0.25,
"tol_minus": 0.25,
"note": "Datum A (resting plane) to the maximum protrusion of the perimeter wells. Secondary sources also quote +/-0.76 mm; consult the document before relying on the tighter band. Lidded and deep-well plates are taller and out of scope of this dimension."
}
},
"fit_checks": [
{"measure": "bbox_z", "dimension": "plate_height"}
]
},
"slas-microplate-flange": {
"title": "Microplate bottom outside flange height",
"authority": "ANSI/SLAS",
"document": "ANSI/SLAS 3-2004 (R2012) Bottom Outside Flange Dimensions",
"url": "https://www.slas.org/SLAS/assets/File/public/standards/ANSI_SLAS_3-2004_BottomOutsideFlangeDimensions.pdf",
"verified": true,
"dimensions": {
"flange_height_short": {"nominal": 2.41, "tol_plus": 0.38, "tol_minus": 0.38},
"flange_height_medium": {"nominal": 6.10, "tol_plus": 0.38, "tol_minus": 0.38},
"flange_height_tall": {"nominal": 7.62, "tol_plus": 0.38, "tol_minus": 0.38}
},
"fit_checks": [],
"design_note": "Three flange heights are standardised. A gripper or carrier that assumes one will drop plates built to another. Ask which the user has."
},
"slas-well-positions-96": {
"title": "96-well plate well positions",
"authority": "ANSI/SLAS",
"document": "ANSI/SLAS 4-2004 (R2012) Well Positions",
"url": "https://www.slas.org/SLAS/assets/File/public/standards/ANSI_SLAS_4-2004_WellPositions.pdf",
"verified": true,
"dimensions": {
"well_pitch": {"nominal": 9.0, "tol_plus": 0.0, "tol_minus": 0.0, "note": "Centre-to-centre in both x and y."},
"a1_offset_x": {"nominal": 14.38, "tol_plus": 0.0, "tol_minus": 0.0, "note": "Left outside edge to the centre of column 1."},
"a1_offset_y": {"nominal": 11.24, "tol_plus": 0.0, "tol_minus": 0.0, "note": "Top outside edge to the centre of row A."},
"well_position_tolerance": {"nominal": 0.70, "tol_plus": 0.0, "tol_minus": 0.0, "note": "Each well centre lies within a 0.70 mm diameter of nominal. This is a positional tolerance zone, not a +/- band."}
},
"fit_checks": [],
"design_note": "Grid layout: x = a1_offset_x + 9.0 * column_index, y = a1_offset_y + 9.0 * row_index, measured from the plate outline corner."
},
"slas-well-positions-384": {
"title": "384-well plate well positions",
"authority": "ANSI/SLAS",
"document": "ANSI/SLAS 4-2004 (R2012) Well Positions",
"url": "https://www.slas.org/SLAS/assets/File/public/standards/ANSI_SLAS_4-2004_WellPositions.pdf",
"verified": false,
"dimensions": {
"well_pitch": {"nominal": 4.5, "tol_plus": 0.0, "tol_minus": 0.0, "note": "Centre-to-centre in both x and y."},
"a1_offset_x": {"nominal": 12.13, "tol_plus": 0.0, "tol_minus": 0.0, "note": "UNVERIFIED. Derived as the 96-well offset minus half the 96-well pitch. Confirm against ANSI/SLAS 4-2004 before cutting metal."},
"a1_offset_y": {"nominal": 8.99, "tol_plus": 0.0, "tol_minus": 0.0, "note": "UNVERIFIED. Derived, as above. Confirm against the document."}
},
"fit_checks": [],
"design_note": "Only well_pitch is confirmed here. Read the standard for the A1 offsets before relying on them."
},
"slas-well-positions-1536": {
"title": "1536-well plate well positions",
"authority": "ANSI/SLAS",
"document": "ANSI/SLAS 4-2004 (R2012) Well Positions",
"url": "https://www.slas.org/SLAS/assets/File/public/standards/ANSI_SLAS_4-2004_WellPositions.pdf",
"verified": false,
"dimensions": {
"well_pitch": {"nominal": 2.25, "tol_plus": 0.0, "tol_minus": 0.0, "note": "Centre-to-centre in both x and y."},
"a1_offset_x": {"nominal": 11.005, "tol_plus": 0.0, "tol_minus": 0.0, "note": "UNVERIFIED. Derived from the 96-well offset. Confirm against ANSI/SLAS 4-2004."},
"a1_offset_y": {"nominal": 7.865, "tol_plus": 0.0, "tol_minus": 0.0, "note": "UNVERIFIED. Derived, as above. Confirm against the document."}
},
"fit_checks": [],
"design_note": "Only well_pitch is confirmed here."
},
"cuvette-standard-10mm": {
"title": "Standard 10 mm path-length spectrophotometer cuvette",
"authority": "De facto industry convention",
"document": "No single ANSI/ISO document fixes this; it is a near-universal convention across suppliers.",
"url": "https://spectrecology.com/blog/guide-to-cuvettes/",
"verified": true,
"dimensions": {
"external_width": {"nominal": 12.5, "tol_plus": 0.1, "tol_minus": 0.1, "note": "Tolerance is indicative; suppliers vary."},
"external_depth": {"nominal": 12.5, "tol_plus": 0.1, "tol_minus": 0.1},
"external_height": {"nominal": 45.0, "tol_plus": 0.5, "tol_minus": 0.5, "note": "Body height excluding any cap or stopper. Semi-micro and micro cuvettes share the external footprint but differ in height and internal geometry."},
"path_length": {"nominal": 10.0, "tol_plus": 0.0, "tol_minus": 0.0, "note": "Internal optical path. 12.5 external minus 2 x 1.25 mm wall."},
"wall_thickness": {"nominal": 1.25, "tol_plus": 0.0, "tol_minus": 0.0}
},
"fit_checks": [
{"measure": "bbox_x", "dimension": "external_width"},
{"measure": "bbox_y", "dimension": "external_depth"}
],
"design_note": "Because this is a convention rather than a standard, a holder should be designed with generous clearance or a compliant feature. Confirm against the user's actual cuvettes."
},
"optical-breadboard-metric": {
"title": "Metric optical breadboard hole grid",
"authority": "De facto industry convention",
"document": "Universal across Thorlabs, Newport, Edmund and others for metric tables.",
"url": "https://www.thorlabs.com/imperial-and-metric-threading",
"verified": true,
"dimensions": {
"grid_pitch": {"nominal": 25.0, "tol_plus": 0.0, "tol_minus": 0.0, "note": "Metric grid. NOT interchangeable with the 25.4 mm imperial grid."},
"thread": {"nominal": 6.0, "tol_plus": 0.0, "tol_minus": 0.0, "note": "M6 x 1.0 tapped holes."},
"clearance_hole_close": {"nominal": 6.4, "tol_plus": 0.0, "tol_minus": 0.0, "note": "Close-fit clearance for an M6 cap screw."},
"clearance_hole_normal": {"nominal": 6.6, "tol_plus": 0.0, "tol_minus": 0.0, "note": "Normal-fit clearance for M6. Prefer this on printed parts."},
"counterbore_dia": {"nominal": 11.0, "tol_plus": 0.0, "tol_minus": 0.0, "note": "For an M6 socket head cap screw head (nominal head dia 10 mm)."},
"screw_head_height": {"nominal": 6.0, "tol_plus": 0.0, "tol_minus": 0.0, "note": "M6 socket head cap screw head height (ISO 4762). A counterbore shallower than this leaves the head proud, not flush."},
"border": {"nominal": 12.5, "tol_plus": 0.0, "tol_minus": 0.0, "note": "Typical edge-to-first-hole distance. A property of the TABLE, not of your part: your plate's edge margin is a free design choice, so do not declare this as an interface."}
},
"fit_checks": [],
"design_note": "Slot rather than hole one of any pair of mounting features to absorb grid and print tolerance."
},
"optical-breadboard-imperial": {
"title": "Imperial optical breadboard hole grid",
"authority": "De facto industry convention",
"document": "Universal across Thorlabs, Newport, Edmund and others for imperial tables.",
"url": "https://www.thorlabs.com/imperial-and-metric-threading",
"verified": true,
"dimensions": {
"grid_pitch": {"nominal": 25.4, "tol_plus": 0.0, "tol_minus": 0.0, "note": "1 inch exactly. Over four holes this differs from the metric grid by 1.6 mm."},
"thread_major_dia": {"nominal": 6.35, "tol_plus": 0.0, "tol_minus": 0.0, "note": "1/4-20 UNC: 0.25 inch major diameter, 20 threads per inch."},
"clearance_hole_normal": {"nominal": 6.8, "tol_plus": 0.0, "tol_minus": 0.0, "note": "Normal-fit clearance for a 1/4-20 screw."},
"counterbore_dia": {"nominal": 11.2, "tol_plus": 0.0, "tol_minus": 0.0, "note": "For a 1/4-20 socket head cap screw head."},
"screw_head_height": {"nominal": 6.35, "tol_plus": 0.0, "tol_minus": 0.0, "note": "1/4-20 socket head cap screw head height (0.25 inch). A counterbore shallower than this leaves the head proud, not flush."},
"border": {"nominal": 12.7, "tol_plus": 0.0, "tol_minus": 0.0, "note": "Typical 0.5 inch edge-to-first-hole distance. A property of the TABLE, not of your part: your plate's edge margin is a free design choice, so do not declare this as an interface."}
},
"fit_checks": [],
"design_note": "Ask which table the user has. Assuming the wrong system is the most common optomechanical design error."
},
"cage-system-30mm": {
"title": "30 mm cage system",
"authority": "Thorlabs (de facto standard, second-sourced by others)",
"document": "Thorlabs 30 mm cage system construction rods and cage plates",
"url": "https://www.thorlabs.com/newgrouppage9.cfm?objectgroup_ID=2273",
"verified": true,
"dimensions": {
"rod_spacing": {"nominal": 30.0, "tol_plus": 0.0, "tol_minus": 0.0, "note": "Rod centre to rod centre, on a square pattern. 1.18 inch."},
"rod_diameter": {"nominal": 6.0, "tol_plus": 0.0, "tol_minus": 0.0, "note": "ER series cage rods."},
"plate_thickness_typical": {"nominal": 8.9, "tol_plus": 0.0, "tol_minus": 0.0, "note": "0.35 inch, the CP33 standard cage plate. Informational, not a mating dimension: custom plates may be any thickness, but matching it keeps optical path budgets simple."}
},
"fit_checks": [],
"design_note": "The 30 mm rod square is centred on the optical axis. A custom plate must place its aperture at the centroid of the four rod bores. Bore diameter is rod_diameter plus twice the free-sliding per-side clearance for YOUR process (fabrication-limits.md): about 6.2 CNC, 6.4 SLA, 6.8 FDM. 6.1 is a reamed-metal number and binds on printed parts. Four bores on a common square over-constrain each other, so do not go tighter than the fits table. Declare the bore against rod_diameter with intent envelope and the clearance you chose."
},
"sm1-lens-tube-thread": {
"title": "SM1 lens tube thread",
"authority": "Thorlabs (de facto standard)",
"document": "Thorlabs SM1 series threading",
"url": "https://www.thorlabs.com/newgrouppage9.cfm?objectgroup_id=4114",
"verified": true,
"dimensions": {
"thread_major_dia": {"nominal": 26.289, "tol_plus": 0.0, "tol_minus": 0.0, "note": "1.035 inch-40 thread. Holds 1 inch diameter optics."},
"threads_per_inch": {"nominal": 40.0, "tol_plus": 0.0, "tol_minus": 0.0},
"pitch": {"nominal": 0.635, "tol_plus": 0.0, "tol_minus": 0.0, "note": "25.4 / 40 mm."},
"optic_dia": {"nominal": 25.4, "tol_plus": 0.0, "tol_minus": 0.0, "note": "1 inch optic."}
},
"fit_checks": [],
"design_note": "A 40 TPI thread has a 0.635 mm pitch, which is at or below the resolution of most FDM printers. Print a clearance bore and use a purchased SM1 adapter or a tapped insert rather than printing the thread."
}
}
}
@@ -0,0 +1,376 @@
---
title: "build123d 0.11.1 patterns"
task: ""
lineage_type: import
upstream_source: https://github.com/K-Dense-AI/scientific-agent-skills/blob/336c4f83/skills/lab-hardware-cad/references/build123d-patterns.md
upstream_sha: 336c4f83
imported_at: 2026-08-16
prompt_class: unknown
upstream_changes: accepted
author: upstream
validated: false
---
# build123d 0.11.1 patterns
An API cookbook for the geometry this skill actually needs. Every snippet here was run against
build123d 0.11.1 on Python 3.12.
## Builder mode or algebra mode
build123d offers two equivalent APIs.
```python
# Builder mode: a context manager collects operations. mode= controls the boolean.
with BuildPart() as ex:
Box(80.0, 60.0, 10.0)
Cylinder(radius=11.0, height=10.0, mode=Mode.SUBTRACT)
part = ex.part
# Algebra mode: plain objects and operators.
part = Box(80.0, 60.0, 10.0) - Cylinder(radius=11.0, height=10.0)
```
**Use builder mode for parts in this skill.** Selectors (`ex.edges()`, `ex.faces()`) read naturally
from the builder, which is what you need for fillets and for placing features on found faces.
Algebra mode is a good fit for short, purely constructive shapes.
Do not mix the two styles inside one `build()`.
## The model file contract
`gen.py` imports the module, calls `build()`, and then reads `interfaces()`. Parameters must be
module-level so they can be overridden with `--param`.
```python
"""One-line description of the part.
Process: SLA, tough resin. Orientation: bore axis vertical.
Interfaces:
- Rod bores: 30 mm cage system, Thorlabs ER series (cage-system-30mm).
"""
from build123d import *
# --- INTERFACE (fixed; do not tune) ---
rod_spacing_mm = 30.0 # cage-system-30mm
rod_bore_d_mm = 6.4 # rod_diameter 6.0 + 2 x 0.20 SLA free-sliding (fabrication-limits.md)
# --- DESIGN (free) ---
plate_t_mm = 8.9
aperture_d_mm = 25.4
def interfaces() -> list[dict]:
return [
{"feature": "cage rod bore spacing", "standard": "cage-system-30mm",
"dimension": "rod_spacing", "value": rod_spacing_mm, "intent": "match"},
{"feature": "cage rod bore diameter", "standard": "cage-system-30mm",
"dimension": "rod_diameter", "value": rod_bore_d_mm,
"intent": "envelope", "clearance": 0.4},
]
def build() -> Part:
half = rod_spacing_mm / 2
with BuildPart() as plate:
Box(rod_spacing_mm + 12.0, rod_spacing_mm + 12.0, plate_t_mm)
with Locations((half, half), (-half, half), (half, -half), (-half, -half)):
Hole(radius=rod_bore_d_mm / 2)
Hole(radius=aperture_d_mm / 2)
return plate.part
```
## Declaring interfaces
Most lab-hardware interfaces are **internal features** — a pocket, a bore, a slot — and none of
them appear in the part's outer bounding box. So `check.py fit` cannot find them by measuring the
STEP, and hand-copying the number into `--value` reintroduces exactly the transcription error the
skill exists to prevent. Declaring them closes the loop: `gen.py` records the declaration in the
manifest, and `check.py interfaces` verifies every entry.
Each entry needs `standard`, `dimension`, and `value`; `feature`, `intent`, and `clearance` are
optional:
| Key | Meaning |
| --- | --- |
| `standard` | ID from `check.py standards --list` |
| `dimension` | a dimension name inside that standard |
| `value` | the number **this model computed**, in mm |
| `feature` | human label for the check output (default: the dimension name) |
| `intent` | `match` if this part must itself conform; `envelope` if the feature must accept any conforming part (default: `match`) |
| `clearance` | total intended clearance in mm, both sides (default: 0) |
**Write `interfaces()` as a function, and compute derived dimensions inside functions.** A
module-level `INTERFACES = [...]` list is also accepted, but it is evaluated at import — before
`--param` is applied — so any value derived from an overridden parameter is recorded wrong. The same
applies to the geometry: derive inside `build()` or a helper, never at module level.
```python
# Wrong: --param plate_tol_mm=0 silently leaves pocket_l_mm at the old value
pocket_l_mm = plate_l_mm + plate_tol_mm + 2 * pocket_clearance_mm
# Right: recomputed on every call, so overrides land
def pocket_l_mm() -> float:
return plate_l_mm + plate_tol_mm + 2 * pocket_clearance_mm
```
`gen.py` warns when it sees a static `INTERFACES` list together with `--param`.
## Declaring geometry checks
`interfaces()` compares declared numbers against the standards database; it never touches the
solid. `checks()` is its measured counterpart: a list of **go/no-go gauges** evaluated by boolean
intersection against the part `build()` actually produced. `gen.py` runs them on every
generation and fails the build if one fails; `check.py geometry` re-runs them against an
exported STEP.
The principle: **every geometric requirement in the request maps to one entry.** Something must
pass through (a screw, a beam, a probe) → a `clear` region. Something must fit into a void (a
plate into a pocket) → a `clear` box the size of the mating part at maximum material condition.
Something must remain (a ridge, a ledge, a screw seat) → a `material` region. A stated size
limit → a `bbox_*` bound. These are exactly the errors `is_valid`, the bounding box, and a
declared-number check cannot see.
```python
def checks() -> list[dict]:
top = plate_t_mm / 2
return [
# a clear region: no material may intrude (screw shafts, through the part)
{"feature": "M6 screws pass all four bores",
"clear": {"cylinder": 6.0, "axis": "z", "at": bolt_xy()}},
# a keep-out with an explicit span (a beam corridor along x at height z)
{"feature": "beam clear at 15 mm above the bench",
"clear": {"cylinder": 5.0, "axis": "x", "at": [(0.0, 15.0)]}},
# a gauge part that must drop into a pocket: the mating part at MMC
{"feature": "SLAS plate at MMC drops into the pocket",
"clear": {"box": (128.01, 85.73, pocket_depth_mm()),
"at": [(0.0, 0.0, floor_t_mm + pocket_depth_mm() / 2)]}},
# a counterbore that really is a counterbore: recess open, seat present.
# The second entry is what catches a recess that punched through.
{"feature": "counterbore recess open at the top",
"clear": {"cylinder": cbore_d_mm - 0.2, "axis": "z", "at": bolt_xy(),
"span": (top - cbore_depth_mm + 0.1, top + 0.1)}},
{"feature": "screw seat present below the recess",
"material": {"cylinder": cbore_d_mm - 0.2, "axis": "z", "at": bolt_xy(),
"span": (-top + 0.1, top - cbore_depth_mm - 0.1)},
"min_mm3": 50.0},
# a user-stated hard limit, measured from the solid
{"feature": "clears the objective turret", "bbox_z": {"max": 15.0}},
]
```
Semantics:
| Key | Meaning |
| --- | --- |
| `clear` / `material` | region that must contain no material / must contain material |
| `{"cylinder": DIA, "axis": "x"\|"y"\|"z", "at": [(a, b), ...], "span": (lo, hi)}` | `at` is 2D in the plane perpendicular to the axis — axis `z`: (x, y); axis `x`: (y, z); axis `y`: (x, z). Omit `span` to run through the whole part |
| `{"box": (dx, dy, dz), "at": [(x, y, z), ...]}` | axis-aligned box gauges centred at each position |
| `tol_mm3` / `min_mm3` | pass thresholds per position (both default 0.01) |
| `bbox_x``bbox_z`, `bbox_min/mid/max` | `{"min": mm, "max": mm}` bounds on the measured bounding box |
Size the gauges from the same named constants as the geometry **only when the requirement is
relational** (the recess sits above the seat). When the requirement is absolute — a mating part's
MMC, a user's height limit, a beam position — write the gauge from the requirement's own numbers,
so a wrong parameter cannot shrink the gauge to match the wrong geometry.
For a one-off question without editing the model, `check.py probe` runs a single gauge from the
command line, and `check.py bores` prints a census of every cylindrical face (diameter, axis,
position, span, sweep) to reconcile against the model's intent.
## Positioning
`Locations` places the objects created inside it. It is the workhorse for bolt patterns.
```python
with Locations((10.0, 0.0), (-10.0, 0.0)): # two positions on the current plane
Hole(radius=3.3)
with Locations((0.0, 0.0, floor_t_mm)): # offset in z
Box(10.0, 10.0, 5.0, mode=Mode.SUBTRACT)
with GridLocations(9.0, 9.0, 12, 8): # x spacing, y spacing, x count, y count
Hole(radius=1.5)
```
`GridLocations` centres the grid on the origin. A microplate well grid is dimensioned from the
plate corner instead, so compute absolute positions and pass them to `Locations`:
```python
a1_x_mm, a1_y_mm, pitch_mm = 14.38, 11.24, 9.0 # slas-well-positions-96
origin_x = -plate_l_mm / 2
origin_y = plate_w_mm / 2
wells = [
(origin_x + a1_x_mm + pitch_mm * col, origin_y - a1_y_mm - pitch_mm * row)
for row in range(8) for col in range(12)
]
with Locations(*wells):
Hole(radius=well_clear_d_mm / 2)
```
## Alignment
By default objects are centred on the origin. `align` moves the datum, which is usually what you
want for a pocket that starts at a floor:
```python
Box(x, y, z, align=(Align.CENTER, Align.CENTER, Align.MIN)) # sits on z = 0
Box(x, y, z, align=(Align.MIN, Align.MIN, Align.MIN)) # corner at the origin
```
Getting this wrong is the classic "pocket cut through the floor" bug, and it is exactly what the
snapshot catches.
## Holes
`Hole` cuts through the whole part; `CounterBoreHole` and `CounterSinkHole` add a head recess.
**`CounterBoreHole` cuts downward from the workplane it is placed on, with the recess at that
plane.** On a centred `Box` the default workplane is the mid-height of the part, so a 2-tuple
location buries the screw seat inside the plate — or, on a thin plate, lets the recess swallow the
top entirely, leaving a straight bore the screw head falls through. Place it on the **top face**
(or give the location an explicit z at the top):
```python
with BuildPart() as plate:
Box(60.0, 60.0, 10.0) # spans z = -5 .. +5
top = plate.faces().sort_by(Axis.Z)[-1]
with Locations(top):
with Locations((20.0, 20.0)):
CounterBoreHole(radius=6.6 / 2, counter_bore_radius=11.0 / 2,
counter_bore_depth=6.5)
```
Size `counter_bore_depth` from the **screw head height**, not from habit: an M6 socket head cap
screw head is 6.0 mm tall, a 1/4-20 head 6.35 mm (`screw_head_height` in the breadboard
standards). A 4 mm counterbore leaves either head 2 mm proud — do not call that flush. After
generating, confirm in the snapshot (or a section) that the recess is at the top face and the
seat ledge exists; both failure modes here pass `is_valid` and the bounding box untouched.
Remember that printed holes come out undersize — see `references/fabrication-limits.md`.
## Selectors
Selectors find edges and faces to fillet, chamfer, or build on. The three you need:
```python
part.edges().filter_by(Axis.Z) # keep edges parallel to Z (the vertical corners)
part.edges().group_by(Axis.Z)[-1] # the group with the highest Z (the top edges)
part.faces().sort_by(Axis.Z)[-1] # the single highest face
part.edges().filter_by(GeomType.CIRCLE) # only circular edges
```
`filter_by` keeps everything matching. `group_by` partitions into lists ordered by the key, so
`[-1]` is the last group and `[0]` the first. `sort_by` orders individual items.
```python
with BuildPart() as ex:
Box(80.0, 60.0, 10.0)
chamfer(ex.edges().group_by(Axis.Z)[-1], length=4.0) # chamfer the top face edges
fillet(ex.edges().filter_by(Axis.Z), radius=5.0) # round the vertical corners
```
**These broad selectors are only safe on a part that is still a plain box.** Once the part has
pockets, bores, notches, or micro-relief, `filter_by(Axis.Z)` and `group_by(Axis.Z)[-1]` also
select the edges of those features, and the fillet either throws a kernel error
(`Failed creating a fillet`, `BRep_API: command not done`) or — worse — succeeds and silently eats
a wall or a 0.3 mm ridge. Both happen in practice. So:
- Fillet or chamfer the **outer body before adding internal features**, or filter the selection
down deliberately (by position, length, or `GeomType`) so only the intended edges remain.
- Bound the radius with `part.max_fillet(edges)` when the nearby geometry is tight — it returns
the largest radius the kernel can actually build on that edge set.
- Make every fillet/chamfer radius a named parameter, and on a kernel failure back the value off
rather than fighting the selector.
- Then check the snapshot: a consumed feature is obvious in the picture and invisible in
`is_valid`.
## Sketch then extrude
For a profile that is not a primitive, sketch it and extrude:
```python
with BuildPart() as bracket:
with BuildSketch() as profile:
Rectangle(40.0, 20.0)
with Locations((15.0, 0.0)):
Circle(radius=4.0, mode=Mode.SUBTRACT)
extrude(amount=6.0)
```
This is also the route to a laser-cut DXF: the sketch is the cut profile.
## Exports
`gen.py` handles these, but for reference:
```python
export_step(part, "part.step", unit=Unit.MM) # authoritative
export_stl(part, "part.stl", tolerance=1e-3, angular_tolerance=0.1)
# 2D profile for laser cutting. section() is a module-level operation, NOT a
# method on the shape -- part.section(...) raises AttributeError.
from build123d.exporters import ColorIndex # NOT exported by `from build123d import *`
profile = section(part, Plane.XY.offset(z_mm), mode=Mode.PRIVATE)
profile = profile.moved(Location((0, 0, -z_mm))) # back to z = 0, or the DXF writer
# warns about a non-planar shape
exporter = ExportDXF(unit=Unit.MM)
exporter.add_layer("CUT", color=ColorIndex.RED) # laser shops key power/speed to layers
exporter.add_shape(profile, layer="CUT")
exporter.write("part.dxf")
```
Cut the section through material, not at `z = 0`: a part modelled sitting on the build plate has
only a degenerate face there. `gen.py --dxf` defaults to the part's mid-height and takes `--dxf-z`
to override.
STEP preserves exact BREP geometry; STL is a triangulated approximation. **Always keep STEP as the
source of truth** and regenerate meshes from it, never the reverse.
## Measuring in code
Useful for asserting an interface inside the model itself:
```python
bbox = part.bounding_box()
print(bbox.size.X, bbox.size.Y, bbox.size.Z)
print(part.volume, part.area)
print(part.is_valid) # a property in 0.11.1, not a method
print(part.center(CenterOf.MASS))
```
`is_valid` being a property rather than a method is a real difference from older releases and from
some documentation. Access it without parentheses.
## Things that bite
- **`is_valid` is a property.** `part.is_valid()` raises `TypeError: 'bool' object is not callable`.
- **`section()` is a module-level operation, not a method.** `part.section(Plane.XY)` raises
`AttributeError`. Call `section(part, plane, mode=Mode.PRIVATE)`.
- **`intersect()` returns a `ShapeList`** with no `.volume`; the `&` operator returns a `Solid` that
has one. `check.py clearance` handles both.
- **Never name a script `inspect.py`** in a directory that lands on `sys.path`. It shadows the
standard library `inspect` module, which breaks `typing_extensions` and therefore build123d
itself. This is why the bundled script is `check.py`.
- **Builder objects are not parts.** Return `builder.part`, not the builder.
- **`Mode.SUBTRACT` needs an existing body.** Subtracting from an empty context does nothing
silently.
- **A swept or extruded profile is centred on its path/plane unless you align it.** Sweeping a
`Rectangle(w, h)` along a path on a surface leaves half the profile below the surface — a
"0.3 mm ridge" that is really 0.15 mm proud. Pass `align=` (and an explicit `x_dir` on the
profile plane) so the profile sits where you think it does, then measure the result.
- **`Curve` has no `.length`.** Sum the edges instead: `sum(e.length for e in curve.edges())`.
- **The boolean of touching or disjoint solids is empty, not an error.** Depending on the path you
get `None`, an empty `Compound`, or a `ShapeList` with no `.volume` — guard before reading
`.volume` in any interference check.
- **`ColorIndex` and `LineType` live in `build123d.exporters`**, not in the top-level namespace;
`from build123d import *` does not bring them in, and `add_layer(color=1)` fails.
- The OpenCascade kernel raises assorted exception types. Catch broadly around boolean operations
and report the failure rather than letting a traceback escape.
## Sources
- build123d documentation — <https://build123d.readthedocs.io/en/latest/>
- Introductory examples (builder vs algebra, selectors, fillets) —
<https://build123d.readthedocs.io/en/latest/introductory_examples.html>
- Import/export reference — <https://build123d.readthedocs.io/en/latest/import_export.html>
@@ -0,0 +1,169 @@
---
title: "Fabrication limits, tolerances, and materials"
task: ""
lineage_type: import
upstream_source: https://github.com/K-Dense-AI/scientific-agent-skills/blob/336c4f83/skills/lab-hardware-cad/references/fabrication-limits.md
upstream_sha: 336c4f83
imported_at: 2026-08-16
prompt_class: unknown
upstream_changes: accepted
author: upstream
validated: false
---
# Fabrication limits, tolerances, and materials
Read this before finalising any geometry. Process determines what geometry is possible; material
determines whether the part survives the lab.
## Process tolerances
Achievable tolerance and minimum feature size, as planning figures. **Every number here depends on
the specific machine, material, and operator.** Use them to choose a process and to size a first
article, then verify with a test coupon.
| Process | Typical tolerance | Min wall | Min feature | Notes |
| --- | --- | --- | --- | --- |
| FDM | ±0.3 mm (often worse over 100 mm) | 1.2 mm (3 x 0.4 mm nozzle) | ~0.8 mm | Anisotropic: much weaker across layers. Porous. |
| SLA / DLP | ±0.1 mm | 0.8 mm | ~0.3 mm | Better surface and detail. Resin choice dominates properties. |
| SLS (nylon) | ±0.2 mm | 0.8 mm | ~0.5 mm | Isotropic, no supports, slightly porous surface. |
| CNC milling | ±0.05 mm or better | 0.8 mm in metal | Set by tool diameter | Internal corners carry the tool radius — you cannot mill a sharp internal corner. |
| Laser cutting | ±0.1 mm | n/a | Kerf ~0.1-0.3 mm | 2D only. Edge taper on thick stock. Kerf offset must be applied. |
Two consequences that catch people:
- **Holes print undersize** on both FDM and SLA. A 6.0 mm modelled hole typically measures under
6.0 mm. Oversize functional bores, or plan to ream them.
- **Internal corners cannot be sharp in milling.** If a milled pocket must accept a square part,
add corner relief cuts. (For a part with *rounded* corners the tool radius is harmless as long
as it stays at or below the part's minimum corner radius — see the corner-radius rule in
`references/labware-adapters.md`.)
### Laser cutting
- **Kerf direction is fixed by the physics, so get it right in the handover.** The beam removes a
strip of width k (~0.10.3 mm) centred on the drawn line. Cutting on the line therefore makes
**holes and internal cutouts come out oversize by ~k, and the part's outer outline undersize by
~k**. Say which convention the DXF uses (on-the-line is the default assumption) and let the shop
offset, or offset the geometry yourself and say so — never both.
- **Put cut geometry on a named layer** (one layer per operation: `CUT`, `ENGRAVE`). Shops key
power and speed to layer or colour; geometry on layer 0 forces them to guess.
- **Cut order matters:** internal features before the outer outline, or the part shifts once it is
freed from the sheet.
- **Sheet stock is not its nominal thickness.** "3 mm" acrylic commonly runs ~2.83.2 mm; slots
sized for nominal will be loose or tight. For solvent-welded joints prefer **cast** acrylic over
extruded — cleaner cut edge, less vapour crazing — and remember alcohols craze acrylic either
way (see Chemical, below).
- Laser-cut edges are sharp and slightly tapered; call out deburring or flame-polishing for
anything handled or animal-facing.
## Fits and clearances
Nominal dimensions do not produce fits. Choose a clearance deliberately, per side:
| Fit | FDM | SLA | CNC |
| --- | --- | --- | --- |
| Free-sliding (a plate dropping into a pocket) | 0.40 mm | 0.20 mm | 0.10 mm |
| Located but removable by hand | 0.25 mm | 0.10 mm | 0.05 mm |
| Press / interference | -0.05 mm | -0.03 mm | -0.02 mm |
Then remember the **other** part has tolerance too. When mating to a standardised component,
design the receiving feature against the component's **maximum material condition**, not its
nominal — a pocket sized from nominal fits only the smaller half of conforming parts. This is what
`intent: "envelope"` enforces. Declare it in the model and check the manifest:
```bash
python scripts/check.py interfaces out/part.manifest.json
```
Or check a single number by hand:
```bash
python scripts/check.py fit --standard slas-microplate-footprint \
--intent envelope --clearance 0.8 --value footprint_length=128.81
```
## Threads and inserts
**Printed threads are usually a mistake.** Layer resolution is comparable to the thread pitch, so
printed threads are weak, dimensionally unreliable, and shed particles.
In descending order of preference:
1. **Heat-set threaded inserts** — the standard solution for printed parts. Model a straight bore
to the insert manufacturer's specified diameter (it varies by insert; get the datasheet) and
provide enough surrounding wall, typically at least 2 mm.
2. **Clearance hole plus a captive nut** in a hex pocket. Reliable and cheap.
3. **Tapping the printed material directly** — acceptable for light, infrequently-assembled joints.
4. **Printing the thread** — only for coarse threads (roughly M6 and above), never for fine
threads like the 0.635 mm pitch SM1 (see `references/optomechanics.md`).
## Orientation and anisotropy
For FDM especially, orientation is a design decision, not a printing detail:
- Parts are substantially weaker **across** layers than along them. Orient so that load runs
along layers, and state the intended orientation in the model docstring.
- Overhangs beyond roughly 45 degrees need support, and supported surfaces come out rough and
dimensionally poor. If a surface is a sealing or mating face, orient it so it is not supported.
- Holes printed with their axis vertical are round; printed horizontally they come out with a
drooped top. Teardrop or chamfer horizontal holes that must stay round.
- **Every enclosed cavity needs a drain path** in resin printing. See
`references/microfluidics.md`.
## Materials
### Thermal
| Material | Approximate service limit | Autoclave (121 °C)? |
| --- | --- | --- |
| PLA | ~50-60 °C | **No** — distorts well below autoclave temperature |
| PETG | ~70-80 °C | No |
| ABS / ASA | ~90-100 °C | Marginal, generally no |
| Polypropylene | ~100 °C | Marginal |
| Nylon (SLS) | ~120-160 °C | Sometimes; verify per grade |
| PEEK | >250 °C | Yes |
| Stainless steel, aluminium, glass | High | Yes |
**Assume a printed part is not autoclavable unless it is a verified high-temperature material.**
Offer chemical or gas sterilisation as the alternative, and check that against the solvent notes
below.
### Chemical
- **Acrylic (PMMA)** crazes on contact with alcohols, including 70% ethanol — a serious problem in
a lab that disinfects everything with ethanol.
- **Polycarbonate** is attacked by many solvents and by some alkaline cleaners.
- **PLA** hydrolyses; it degrades in warm, wet, or repeatedly-cleaned service.
- **PP, PTFE, PEEK** have broad chemical resistance and are the safe choices for solvent contact.
Always ask what the part will be cleaned with, not just what it will contain. Cleaning agent
compatibility is more often the failure than the sample.
### Biocompatibility
- **Uncured SLA resin is cytotoxic.** Even nominally biocompatible resins require the
manufacturer's full post-cure and wash protocol, and leachables can still affect sensitive cell
assays.
- For anything contacting cells, tissue, or animals: prefer glass, medical-grade polymer, or PTFE
for the contact surface, and use the printed part as a holder that does not touch the sample.
- "Biocompatible" on a resin datasheet refers to a specific certified process and application. It
does not transfer to your printer, your cure schedule, or your assay. Say this rather than
implying a printed part is cell-safe.
### Optical
- Printed and milled surfaces scatter; they are not optical surfaces.
- Most printed resins **autofluoresce**, often strongly, which contaminates fluorescence readouts.
- Black is not automatically non-reflective.
- Where an optical surface is needed, use glass or a bonded film and model the holder around it.
## Cost and lead-time reality
Mention these when recommending a process: FDM is hours and pennies; SLA is hours and modest cost;
SLS and CNC are typically outsourced with days of lead time and much higher cost. A design that
needs ±0.05 mm has committed the user to CNC — flag that trade before they discover it at quoting.
## Before fabrication
Work through `references/validation.md`.
@@ -0,0 +1,203 @@
---
title: "Labware adapters, holders, and racks"
task: ""
lineage_type: import
upstream_source: https://github.com/K-Dense-AI/scientific-agent-skills/blob/336c4f83/skills/lab-hardware-cad/references/labware-adapters.md
upstream_sha: 336c4f83
imported_at: 2026-08-16
prompt_class: unknown
upstream_changes: accepted
author: upstream
validated: false
---
# Labware adapters, holders, and racks
Parts that receive standard consumables: microplates, cuvettes, tubes, slides, dishes.
The governing principle: **where a published standard exists, design to the standard; where it
does not, require a measurement.** Microplate footprints are standardised. Well geometry, skirt
profiles, tube dimensions, and lid fits are not.
Verified dimensions live in `assets/standards.json`. Query them rather than copying numbers:
```bash
python scripts/check.py standards --show slas-microplate-footprint
```
## Microplates (ANSI/SLAS 1-4)
Four documents split the plate geometry. All are ANSI-approved and were reaffirmed in 2012.
| Document | Governs | Key numbers |
| --- | --- | --- |
| ANSI/SLAS 1-2004 | Footprint | 127.76 x 85.48 mm ±0.25; corner radius 3.18 ±1.6 mm |
| ANSI/SLAS 2-2004 | Height | 14.35 ±0.25 mm, resting plane to top of perimeter wells |
| ANSI/SLAS 3-2004 | Bottom outside flange | Short 2.41, medium 6.10, tall 7.62 mm, each ±0.38 |
| ANSI/SLAS 4-2004 | Well positions | 96-well: 9.0 mm pitch, A1 at 14.38 mm from left, 11.24 mm from top |
### Designing a plate pocket
Three traps, in the order people fall into them.
**1. Design to maximum material, not to nominal.** A plate at the top of tolerance is
127.76 + 0.25 = 128.01 mm. A pocket cut at 127.76 + clearance will jam on roughly half the plates
you try. Compute:
```python
plate_l_mm = 127.76 # ANSI/SLAS 1-2004 nominal
plate_tol_mm = 0.25 # ANSI/SLAS 1-2004
fit_clearance_mm = 0.40 # per side; FDM, see fabrication-limits.md
pocket_l_mm = plate_l_mm + plate_tol_mm + 2 * fit_clearance_mm # 128.81
```
**2. The corner radius tolerance is enormous — and it bounds the pocket radius from above,
not below.** 3.18 ±1.6 mm means a real plate corner is anywhere from 1.58 to 4.78 mm. Get the
direction right: a plate corner is **convex**, a pocket fillet is **concave material bulging
inward**, so a *sharp* internal pocket corner always clears a rounded plate — the unused corner is
empty space. It is a pocket fillet *larger* than the plate's corner radius that binds: the bulge
occupies space the plate needs. Sizing the fillet to the plate's maximum corner radius is
therefore exactly backwards — it binds every plate except those at the top of the corner
tolerance.
The safe options, best first:
- **Corner relief** (a small slot or bore cut past each corner) — always clears, prints and mills
cleanly, and is the standard fix.
- **Fillet no larger than the plate's minimum corner radius** (1.58 mm for SLAS plates) — clears
every conforming plate in every position.
- A larger fillet only if `R ≤ r_min + ~3.4 × per-side clearance` — the geometry only recovers the
intrusion when the plate stays roughly centred, so treat this as a last resort and say so.
```python
with BuildPart() as pocket:
# ... pocket geometry ...
# relief bores just outside each pocket corner: clears any conforming corner radius
with Locations(*corner_relief_centres()):
Hole(radius=2.0)
```
**3. Height depends on the flange, not just the plate.** ANSI/SLAS 3 standardises three flange
heights. A carrier that grips the flange must be told which one. Ask; do not assume medium.
### Well grid
For a part that must reach individual wells — a magnet block, a lid with access holes, a light
guide — lay out from the plate's outline corner, not from the plate centre:
```python
a1_x_mm, a1_y_mm, pitch_mm = 14.38, 11.24, 9.0 # ANSI/SLAS 4-2004, 96-well
locations = [
(a1_x_mm + pitch_mm * col, a1_y_mm + pitch_mm * row)
for row in range(8) for col in range(12)
]
```
The standard's positional tolerance is a **0.70 mm diameter zone** around each nominal centre, not
a ±0.70 mm band. A feature that must clear every well needs at least 0.35 mm of radial margin on
top of your own process tolerance.
384-well pitch is 4.5 mm and 1536-well pitch is 2.25 mm. **The A1 offsets for those formats in
`standards.json` are marked unverified** — they were derived, not read from the document. Read
ANSI/SLAS 4-2004 before relying on them.
### What the standards do not fix
Well diameter, well depth, well bottom shape (flat, round, conical), skirt height, lid geometry,
optical bottom thickness, and deep-well plate height. All vary by manufacturer and product line.
If the part touches any of these, get the vendor drawing or measure it.
## Cuvettes
The standard macro cuvette is a convention rather than a published standard, but it is close to
universal: **12.5 x 12.5 mm external, 45 mm tall, 1.25 mm wall, 10 mm optical path**.
Design notes:
- Holders should be generous or compliant. Because no document fixes the tolerance, a 0.1 mm
interference fit designed against nominal will fail on some suppliers' cuvettes.
- Semi-micro and micro cuvettes keep the 12.5 mm external footprint but change internal geometry
and often height. A holder designed for the external footprint accommodates all of them; one
designed around the sample volume does not.
- Cuvettes are usually held with a spring or leaf on one face so the two optical faces register
against fixed datums. Copy that: locate on two adjacent faces, preload from the opposite corner.
A four-sided pocket with clearance lets the cuvette rotate and shifts the path length.
- **Never print the optical path.** Printed surfaces scatter. The cuvette provides the optical
faces; the holder provides position only, and must not obstruct the beam window.
## Tubes
Tube dimensions are **not standardised** and differ measurably between suppliers, and often
between product lines from the same supplier. Approximate outside diameters near the tube rim:
| Tube | Approximate OD | Note |
| --- | --- | --- |
| 0.2 mL PCR | 6 mm | Often supplied in strips or as a 96-format plate |
| 1.5 mL microcentrifuge | 11 mm | Rim is wider than the body; the body tapers |
| 2.0 mL microcentrifuge | 11 mm | Same rim as 1.5 mL, taller body |
| 15 mL conical | 17 mm | Cap is wider than the tube |
| 50 mL conical | 30 mm | Cap is wider than the tube |
**Treat every number in this table as a starting point for a first article, not a design input.**
Ask the user for the supplier and catalogue number, or ask them to measure with calipers. Then
design a rack that holds the tube by the **rim or the cap**, which is dimensionally stable, rather
than by the tapered body, which is not.
For a rack, the useful pattern is a through-hole sized to the body plus clearance and a counterbore
that catches the rim, so the tube hangs rather than bottoms out.
## Microscope slides and coverslips
Standard slide: **75 x 25 mm, 1.0 mm thick** (ISO 8037-1 covers slide dimensions; thickness classes
vary, and 1.0-1.2 mm is typical). Coverslips are specified by thickness number, not dimension:
#1 is roughly 0.13-0.17 mm and #1.5 roughly 0.16-0.19 mm.
Objective working distance is unforgiving. A holder that adds even 0.2 mm under the slide can put
the sample outside a high-NA objective's working distance. Design slide holders so the slide
registers directly against the stage datum, with the holder clamping from above.
## Petri dishes and stage inserts
Standard dish outside diameters are approximately 35, 60, 90, and 100 mm, but the flange profile
and lid fit vary. Dishes are also slightly out of round. Locate on three points rather than a
continuous circular pocket: a three-point nest is insensitive to ovality, a close-fitting bore is
not.
For stage inserts, the interface that matters is the **microscope stage opening**, which is
instrument-specific and must be measured. Many stages accept a standard SLAS-footprint insert;
confirm before assuming it.
## Checks to run
Declare the pocket in the model's `interfaces()` and let the check read it:
```bash
python scripts/gen.py carrier_model.py --outdir out/
python scripts/check.py interfaces out/carrier.manifest.json
```
**Do not point `check.py fit` at the carrier's STEP.** `fit` measures the outer bounding box, which
for a carrier is the outside of its walls — 6 mm larger than the pocket here — so it fails against
the plate footprint no matter how correct the pocket is. The dimension that matters is internal, so
it has to be declared, not measured from the envelope.
To check the number by hand instead:
```bash
python scripts/check.py fit --standard slas-microplate-footprint \
--intent envelope --clearance 0.8 --value footprint_length=128.81
```
`--intent envelope` checks one-sided against maximum material condition, and `--clearance` is the
total intended clearance: 0.40 mm per side is 0.80 mm. Passing means the pocket is the size you
intended, not that the plate fits — only a test print shows that.
Then always run `snapshot.py` and confirm the pocket is on the face you meant.
## Sources
- ANSI/SLAS 1-2004 (R2012) Footprint Dimensions — <https://www.slas.org/SLAS/assets/File/public/standards/ANSI_SLAS_1-2004_FootprintDimensions.pdf>
- ANSI/SLAS 2-2004 (R2012) Height Dimensions — <https://www.slas.org/SLAS/assets/File/public/standards/ANSI_SLAS_2-2004_HeightDimensions.pdf>
- ANSI/SLAS 3-2004 (R2012) Bottom Outside Flange Dimensions — <https://www.slas.org/SLAS/assets/File/public/standards/ANSI_SLAS_3-2004_BottomOutsideFlangeDimensions.pdf>
- ANSI/SLAS 4-2004 (R2012) Well Positions — <https://www.slas.org/SLAS/assets/File/public/standards/ANSI_SLAS_4-2004_WellPositions.pdf>
- SLAS microplate standards overview — <https://www.slas.org/education/ansi-slas-microplate-standards/>
@@ -0,0 +1,170 @@
---
title: "Microfluidic chips, molds, and flow cells"
task: ""
lineage_type: import
upstream_source: https://github.com/K-Dense-AI/scientific-agent-skills/blob/336c4f83/skills/lab-hardware-cad/references/microfluidics.md
upstream_sha: 336c4f83
imported_at: 2026-08-16
prompt_class: unknown
upstream_changes: accepted
author: upstream
validated: false
---
# Microfluidic chips, molds, and flow cells
Channel networks, soft-lithography molds, printed chips, gaskets, and manifolds.
## First: decide what you are actually modelling
This is the error that wastes the most time in microfluidic CAD. Three different objects get
called "the chip":
| Object | Channels are | Made by |
| --- | --- | --- |
| **Mold / master** | **Raised ridges** (positive relief) | Photolithography on a wafer, SLA print, or micromilling |
| **Cast chip** | **Recessed grooves** (negative of the mold) | PDMS cast against the mold, then bonded to a substrate |
| **Directly-fabricated chip** | **Recessed grooves or enclosed lumens** | Printed, milled, or laser-cut directly |
A model that is correct as a chip is exactly wrong as a mold. Put the polarity in the module
docstring and in a named parameter, and **verify it numerically, not by eye**: inverted polarity
is invisible in the bounding box, the volume, and the validity check — and at typical channel
scale (a 0.3 mm ridge on a 40+ mm part) it is invisible in an outline render too, because raised
and recessed features draw the same edges. Declare it as geometry checks instead
(`references/build123d-patterns.md`): a `material` region where the ridge must stand above the
casting surface, and a `clear` region over the rest of that layer — a groove fails the first,
an inverted full-area layer fails the second. For a one-off question,
`check.py probe <step> --box ... --expect material` answers it without editing the model. State
the measured relief height in the report. Use the snapshot for layout and connectivity, which
it does show well.
```python
polarity = "mold" # "mold" = raised ridges; "chip" = recessed grooves
```
If casting PDMS, the mold also needs a **surrounding wall or a casting frame** to contain the
uncured polymer, and enough flat land around the features for the cast part to release.
## Channel cross-section and aspect ratio
Channels are usually rectangular because that is what planar fabrication produces. Two failure
modes bound the aspect ratio, and both are geometric:
- **Roof sag / collapse** — a channel much wider than it is tall has an unsupported ceiling. In
PDMS the roof bows down and can stick to the floor. Commonly cited guidance keeps
**width : height below roughly 10 : 1**; wide channels need support pillars.
- **Sidewall collapse** — a mold ridge much taller than it is wide falls over or fails to release.
Keep **height : width below roughly 10 : 1** on the mold.
Treat both as rules of thumb, not guarantees: the real limits depend on PDMS mixing ratio, cure
schedule, and applied pressure. For anything load-bearing or high-pressure, prototype.
Also keep **channel-to-channel spacing at least the channel height**, so the wall between two
channels does not deflect or leak, and leave a flat **bonding land** — typically 1 mm or more of
uninterrupted flat surface around the network perimeter — for plasma or adhesive bonding.
## Minimum features by process
Achievable feature size drives the entire design, and the range across processes is three orders
of magnitude. Confirm against your specific tool before committing.
| Process | Practical minimum channel | Notes |
| --- | --- | --- |
| SU-8 photolithography | ~1-10 µm wide, 1-200+ µm tall | The reference process for soft lithography. Feature height is set by spin speed and resist grade. |
| Two-photon / µSLA | ~10-50 µm | Small build volume, slow, expensive. |
| Desktop SLA / DLP | ~200-500 µm | Uncured resin is very hard to clear from smaller lumens. Enclosed channels below ~0.5 mm frequently print blocked. |
| Micromilling | ~100 µm | Set by end-mill diameter; depth limited by tool aspect ratio. Leaves tool marks that scatter light. |
| FDM | Not suitable for sealed channels | Layer porosity leaks. Use only for holders and manifolds. |
| Laser-cut film / gasket | ~200 µm | Excellent for stacked-layer devices and gaskets. |
**Design enclosed printed channels for drainage.** Every lumen needs a path for uncured resin to
escape, and orientation on the build plate determines whether it drains. If the user is printing,
say which way up.
## Ports and tubing
The port is where most chips leak. Options, roughly in order of how common they are in a research
lab:
- **Direct tubing insertion** — a bore slightly *under* the tubing OD so the tubing seals by
interference. For 1/16 inch OD tubing (1.5875 mm), a bore around 1.5 mm in PDMS is typical. This
works in elastomer and fails in rigid printed parts, which crack instead of gripping.
- **Luer taper** — the standard syringe interface, a **6% taper** (ISO 80369-7 supersedes the
legacy ISO 594 series for medical use). Convenient, low pressure only. If you model a Luer taper,
get the profile from the standard, not from memory.
- **Threaded fittings** — flat-bottom **1/4-28 UNF** is the common lab standard for low-pressure
fluidics; **10-32 coned** is used at higher pressures. These need a tapped or heat-set-insert
port and a matching flat sealing face.
- **Barbs** — reliable with soft tubing and a clamp, bulky.
Whichever you choose, the sealing surface must be **flat and normal to the port axis**. A port
face left at a printed layer angle will not seal.
## Dead volume
Dead volume dominates the response time of any perfusion or gradient device, and it is trivially
computable, so compute it rather than estimating:
```
V = pi * r^2 * L # round tubing / bore
V = w * h * L # rectangular channel
```
Report the volume of every connecting bore alongside the channel network volume. A 20 mm long
1 mm bore holds ~15.7 µL, which is often larger than the entire channel network it feeds.
## Flow regime sanity check
Microfluidic flow is almost always laminar, but state it rather than assuming:
```
Re = rho * v * D_h / mu
D_h = 2 * w * h / (w + h) # hydraulic diameter, rectangular channel
```
For water in a 100 µm channel at 1 mm/s, Re is of order 0.1 — deeply laminar, so mixing is
diffusive only. If the design depends on mixing, it needs a mixer geometry (serpentine,
herringbone, or split-and-recombine); relying on turbulence will not work at these scales.
Pressure drop for a rectangular channel scales steeply with the smaller dimension. **Halving
channel height raises pressure drop by roughly an order of magnitude.** Check that the intended
pump or syringe can actually deliver it before finalising the cross-section.
## Material and optical constraints
- **PDMS** absorbs small hydrophobic molecules and is gas-permeable. Both are sometimes features
(oxygenation in organ-on-chip) and sometimes fatal to an assay (drug studies).
- **SLA resins** are frequently cytotoxic uncured and often still after a nominal cure. For cell
work, require post-cure plus a documented biocompatibility check, or use a different process.
See `references/fabrication-limits.md`.
- **Autofluorescence** matters for any fluorescence readout. Most printed resins autofluoresce
strongly. Image through glass or a thin COC/COP film, not through printed material.
- **Optical path**: printed and milled surfaces scatter. Any imaging window should be a bonded
coverslip or film, and the model must specify its thickness so the objective working distance
works out.
## Checks to run
```bash
python scripts/gen.py chip_model.py --outdir out/
python scripts/check.py facts out/chip.step
python scripts/snapshot.py out/chip.step --out out/chip.png
```
`facts` gives the volume; compare it against your hand-computed channel volume as an independent
check that the network is actually open and connected. A network modelled as a solid rather than a
cavity shows up immediately as a volume far larger than expected.
Then read the snapshot and confirm, explicitly:
1. **Polarity** — ridges for a mold, grooves for a chip.
2. Every port lands on the channel it should, and passes fully through to the surface.
3. The bonding land is continuous around the network.
4. No channel has been closed off or consumed by a fillet.
## Sources
- ISO 80369-7 (Luer connectors for intravascular applications) supersedes the ISO 594 series.
Obtain the taper profile from the standard itself.
- Aspect-ratio and spacing guidance here is standard soft-lithography practice; the numerical
limits are rules of thumb and depend on material and process. Prototype before committing.
@@ -0,0 +1,161 @@
---
title: "Optomechanical mounts and breadboard hardware"
task: ""
lineage_type: import
upstream_source: https://github.com/K-Dense-AI/scientific-agent-skills/blob/336c4f83/skills/lab-hardware-cad/references/optomechanics.md
upstream_sha: 336c4f83
imported_at: 2026-08-16
prompt_class: unknown
upstream_changes: accepted
author: upstream
validated: false
---
# Optomechanical mounts and breadboard hardware
Parts that bolt to an optical table, join a cage system, hold an optic or a sample in a beam path,
or carry a camera or objective.
Verified dimensions are in `assets/standards.json`:
```bash
python scripts/check.py standards --show optical-breadboard-metric
python scripts/check.py standards --show cage-system-30mm
python scripts/check.py standards --show sm1-lens-tube-thread
```
## Ask which system before you model anything
**Metric and imperial optical hardware are not interchangeable, and the difference is small enough
to look like a rounding error and large enough to prevent assembly.**
| | Metric | Imperial |
| --- | --- | --- |
| Grid pitch | 25.0 mm | 25.4 mm (1 inch) |
| Tapped hole | M6 x 1.0 | 1/4-20 UNC |
| Typical border | 12.5 mm | 12.7 mm |
Over a four-hole span the grids differ by **1.6 mm** — far more than any clearance hole absorbs.
There is no way to infer which the user has from the request. Ask. If the answer is unavailable,
model the mounting features as **slots along the bolt line** rather than round holes, which
tolerates both, and say that is what you did and why.
## Mounting to the table
- Use **clearance holes, not tapped holes**, in the part. The table is tapped; the part is
clearanced. For M6 use 6.6 mm (normal fit) in a printed part rather than 6.4 mm — printed holes
come out undersize.
- **Counterbore for the screw head** if the part surface must stay clear: roughly 11 mm diameter
for an M6 socket head cap screw, 11.2 mm for 1/4-20.
- **Never rely on more than two holes to locate a part.** Grid tolerance plus print tolerance means
a rigid four-hole pattern will bind. Round hole + slot is the standard fix: one hole locates, the
slot takes up the error.
- Printed parts are compliant. For anything where pointing stability matters, a printed mount is a
prototyping aid, not a final part — thermal drift and creep in polymer are large compared with
optical alignment tolerances. Say so when recommending one.
## Posts and pedestals
Common conventions, which vary by vendor — **confirm against the catalogue before use**:
- Imperial posts are Ø1/2 inch (12.7 mm), typically tapped 8-32 at one end with a 1/4-20 stud or
clearance at the other.
- Metric posts are Ø12 mm, typically tapped M4 with an M6 interface to the table.
- A post-holder plus post is height-adjustable but adds a compliant joint; a pedestal or a
solid machined riser is stiffer.
**Beam height** is a project-wide constant, not a per-part choice. Every mount on the table must
put its optic at the same height. Common conventions are 3 inches (76.2 mm) or 100 mm, but this is
a lab-by-lab choice. Ask for the number, define it once as `beam_height_mm`, and derive every
mount's optic centre from it.
## 30 mm cage system
The dominant convention for small free-space assemblies:
- **Rod spacing 30.0 mm** on a square, centred on the optical axis.
- **Rods Ø6 mm** (ER series).
- Standard cage plates are 0.35 inch (8.9 mm) thick.
For a custom cage plate: place four bores on a 30 mm square, put the aperture at the **centroid**
of those four bores, and bore them for a free-sliding fit **at your process's clearance**
(fabrication-limits.md): about 6.2 mm CNC, 6.4 mm SLA, 6.8 mm FDM. 6.1 mm is a reamed-metal
number — printed bores come out undersize, and four bores on a common square over-constrain each
other, so tighter is not better here. A cage plate that binds on the rods is worse than useless
because it transmits stress into the whole assembly.
Cage plates stack along the rods, so a custom plate's thickness directly consumes optical path
length. Budget it.
## Lens tube threads (SM series)
**SM1 is a 1.035 inch-40 thread**, which holds Ø1 inch (25.4 mm) optics. That is a **0.635 mm
pitch**.
**Do not print SM threads.** A 0.635 mm pitch is at or below the practical resolution of FDM and
marginal on desktop SLA; a printed SM1 thread will either not engage or will gall and shed
particles into the beam path. Instead:
- bore a clearance hole and use a purchased SM1 adapter or retaining ring, or
- design for a threaded metal insert, or
- clamp the optic directly with a retaining flange and screws.
If the design truly requires a printed thread, say explicitly that it needs test printing and is
likely to fail.
## Holding an optic
- **Never clamp an optic on its clear aperture.** Contact only the outer annulus of the face or the
edge. Define `clear_aperture_mm` as a named parameter and confirm in the snapshot that nothing
intrudes on it.
- Three-point contact is kinematically correct and does not deform the optic. A continuous
circular seat over-constrains it and induces stress birefringence, which matters for
polarisation work.
- Leave clearance for thermal expansion. A metal-in-polymer mount that is a press fit at 20 °C can
crack or bind across a temperature swing.
- Retaining forces should be light and distributed. A single set screw pressing on glass is a way
to chip glass.
## Stray light and scatter
Geometry is not the whole design here, and a STEP file cannot show any of this:
- Printed surfaces scatter strongly. Any surface that sees the beam should be baffled, angled away
from the optical axis, or treated.
- **Black does not mean non-reflective.** Black resin and black filament are often quite specular.
Specify a genuinely absorbing surface treatment where it matters.
- Thread and layer lines act as diffraction structures near a focus.
- For fluorescence work, printed material near the sample can autofluoresce into the detection
path.
Flag these to the user; do not silently assume a printed enclosure is light-tight.
## Checks to run
```bash
python scripts/gen.py mount_model.py --outdir out/
python scripts/check.py facts out/mount.step
python scripts/check.py interfaces out/mount.manifest.json
python scripts/snapshot.py out/mount.step --out out/mount.png
```
Declare the grid pitch, rod spacing, and bore diameters in the model's `interfaces()` against
`optical-breadboard-metric`, `optical-breadboard-imperial`, or `cage-system-30mm`, so the check
catches a 25.0-for-25.4 substitution rather than leaving it to a reader.
There is still **no automatic bolt-pattern check** — the interface check compares dimensions, not
hole positions. Compute the pattern in the model from a named `grid_pitch_mm` constant, and confirm
in the snapshot that:
1. All mounting holes are present and pass fully through.
2. The optic aperture is centred where you intended, and unobstructed.
3. Counterbores are on the accessible face.
4. Nothing intrudes into the clear aperture or the beam path.
## Sources
- Thorlabs imperial and metric threading — <https://www.thorlabs.com/imperial-and-metric-threading>
- Thorlabs standard 30 mm cage plates — <https://www.thorlabs.com/newgrouppage9.cfm?objectgroup_ID=2273>
- Thorlabs SM1 lens tube compatible cage plates — <https://www.thorlabs.com/newgrouppage9.cfm?objectgroup_id=4114>
- Post dimensions, beam heights, and vendor-specific thread conventions in this file are common
conventions rather than published standards. Confirm against the catalogue.
@@ -0,0 +1,145 @@
---
title: "Pre-fabrication validation checklist"
task: ""
lineage_type: import
upstream_source: https://github.com/K-Dense-AI/scientific-agent-skills/blob/336c4f83/skills/lab-hardware-cad/references/validation.md
upstream_sha: 336c4f83
imported_at: 2026-08-16
prompt_class: unknown
upstream_changes: accepted
author: upstream
validated: false
---
# Pre-fabrication validation checklist
Work through this before telling a user a part is ready to fabricate. Each item names the failure
it catches, because a checklist without consequences gets skipped.
## 1. Provenance
- [ ] The STEP was produced by `gen.py` from the current model source.
*Catches: a stale artifact that no longer matches the code you just edited.*
- [ ] A `*.manifest.json` exists alongside it, and its `source.sha256` matches the model file.
*Catches: silently editing an exported STEP, which makes the design unreproducible.*
- [ ] The manifest's `interfaces` block lists every dimension a bundled standard covers, and its
values are the ones the model computed after any `--param` override. Empty is correct only
when nothing on the part mates with a bundled standard — and then every interface dimension
is named as unchecked in the report instead.
*Catches: a static `INTERFACES` list frozen at import, recording pre-override numbers; and
an interface that silently escaped checking.*
- [ ] Every parameter in the model is named with units.
*Catches: the bare `12.7` nobody can later identify as half an inch.*
```bash
python scripts/gen.py part_model.py --outdir out/
```
## 2. Geometry is sound
- [ ] `is_valid` is true.
*Catches: self-intersecting or non-manifold solids that slicers and CAM silently mangle.*
- [ ] `solid_count` is what you expect — usually 1.
*Catches: a boolean that failed and left two disjoint lumps, or a feature floating free of
the body.*
- [ ] Volume is plausible for the part's size and wall thickness.
*Catches: a cavity modelled solid, or a subtract that did nothing.*
- [ ] Every geometric requirement in the request is declared in `checks()` and passes — clear
regions for what must pass through or fit in, material regions for what must remain,
bbox bounds for stated size limits.
*Catches: a recess that swallowed its screw seat, a pocket the mating part cannot enter,
a beam corridor with a wall in it, a feature a fillet silently ate — all invisible to
`is_valid` and the bounding box.*
```bash
python scripts/check.py facts out/part.step
python scripts/check.py geometry out/part.step --model part_model.py
```
## 3. Interfaces
- [ ] Every interface dimension has a written source: a standard ID, a vendor drawing, or a user
measurement. **None came from memory.**
*Catches: the single most expensive failure mode in this skill.*
- [ ] Every interface covered by a standard is declared in the model's `interfaces()` and passes
`check.py interfaces`.
*Catches: an interface nobody checked because the outer bounding box could not see it.*
- [ ] Features that receive a standardised component use `intent: "envelope"`.
*Catches: a pocket sized to nominal, which fits only the smaller half of conforming parts.*
- [ ] Any standard entry marked `verified: false` was confirmed against the primary document, or
the user was told it is unconfirmed.
*Catches: propagating a derived number as if it were read from the standard.*
- [ ] Metric vs imperial is confirmed where both exist, and no expression mixes them.
*Catches: the 25.0 vs 25.4 mm grid error, which accumulates to 1.6 mm over four holes.*
- [ ] Interfaces not covered by any bundled standard — a vendor drawing, a measurement — were
reported to the user as unchecked, with the number and its source.
*Catches: a silent gap where the automatic check simply had nothing to say.*
```bash
python scripts/check.py interfaces out/part.manifest.json
# one dimension by hand, when it is not declared in the model
python scripts/check.py fit --standard <id> --intent envelope --clearance <mm> --value <dim>=<mm>
```
## 4. Fits and assembly
- [ ] Every mating dimension has a deliberate clearance chosen for the process.
*Catches: nominal-to-nominal fits, which do not assemble.*
- [ ] Multi-part assemblies were checked for interference.
*Catches: parts that overlap in CAD and therefore cannot exist together.*
- [ ] Rigid multi-hole mounting patterns have at least one slot.
*Catches: a four-hole bolt pattern binding on accumulated tolerance.*
```bash
python scripts/check.py clearance out/a.step out/b.step --min 0.3
```
## 5. Manufacturability
- [ ] Minimum wall and feature sizes are within the chosen process (`fabrication-limits.md`).
- [ ] Print or machining orientation is stated, and load runs along layers, not across them.
- [ ] Threads use inserts or captive nuts rather than printed threads, unless coarse.
- [ ] Enclosed cavities have a drain path for resin, and support-free access where possible.
- [ ] Milled internal corners have relief for the tool radius.
## 6. Material
- [ ] Material is compatible with the **cleaning agent**, not only the sample.
*Catches: acrylic crazing on 70% ethanol; PLA distorting in an autoclave.*
- [ ] Sterilisation method is stated and the material actually survives it.
- [ ] Anything contacting cells, tissue, or animals has a justified material, or contact is
designed out.
*Catches: assuming a printed resin part is cell-safe.*
- [ ] Optical requirements — autofluorescence, scatter, transmission — are addressed if the part is
near a beam or a detector.
## 7. Visual review — mandatory
- [ ] A snapshot was rendered **and read** after the most recent generation.
- [ ] Confirmed in the image: features on the intended faces; correct mold/chip polarity; every
port, bore, and boss present, inside the body, and passing through; nothing consumed by a
fillet; clear apertures unobstructed.
```bash
python scripts/snapshot.py out/part.step --out out/part.png
```
**This step is never waived by the numeric checks passing.** `is_valid: true` with a correct
bounding box is fully consistent with a pocket cut on the wrong face or an inverted mold. Those
errors are obvious in the picture and invisible in the numbers.
## 8. Report
Give the user, explicitly:
1. Process and material, and why.
2. Every interface dimension with its source and tolerance.
3. Clearances chosen, and the fit class they came from.
4. What the snapshot showed — described, not merely "a snapshot was generated".
5. Every check that did not pass, and every dimension you could not verify.
6. A recommendation to print a test coupon of the critical interface before committing to the full
part, whenever the design depends on a fit.
State the unverified items plainly. A part list with one honest "this dimension needs
confirmation" is far more useful than a confident one that is silently wrong.
@@ -0,0 +1,321 @@
---
title: "Rowan Workflow Catalog"
task: ""
lineage_type: import
upstream_source: https://github.com/K-Dense-AI/scientific-agent-skills/blob/72d742e1/skills/rowan/references/workflow_catalog.md
upstream_sha: 72d742e1
imported_at: 2026-08-29
prompt_class: unknown
upstream_changes: accepted
author: upstream
validated: false
---
# Rowan Workflow Catalog
Submission code, options, and result shapes for the common workflow categories, followed
by the complete list of supported workflow types.
## Common workflow categories
### 1. Descriptors
A lightweight entry point for batch triage, SAR, or exploratory scripts.
```python
wf = rowan.submit_descriptors_workflow(
rowan.Molecule.from_smiles("CC(=O)Oc1ccccc1C(=O)O"),
name="aspirin descriptors",
)
result = wf.result()
print(result.descriptors["MW"]) # 180.042 — exact mass
print(result.descriptors["SLogP"]) # 1.31
print(result.descriptors["TopoPSA"]) # 63.6 — topological PSA
print(result.descriptors["nHBAcc"]) # 3.0
```
**Common descriptor keys:**
| Key | Description | Typical drug range |
|-----|-------------|-------------------|
| `MW` | Exact/monoisotopic mass (Da), not average MW | <500 (Lipinski) |
| `SLogP` | Calculated LogP (lipophilicity) | -2 to +5 |
| `TopoPSA` | Topological polar surface area (Ų) | <140 for oral bioavailability |
| `TPSA` | 3D charged surface area, not topological PSA | — |
| `nHBDon` | H-bond donor count | ≤5 (Lipinski) |
| `nHBAcc` | H-bond acceptor count | ≤10 (Lipinski) |
| `nRot` | Rotatable bond count | <10 for oral drugs |
| `nRing` | Ring count | — |
| `nHeavyAtom` | Heavy atom count | — |
| `FilterItLogS` | Estimated aqueous solubility (LogS) | >-4 preferred |
| `Lipinski` | Lipinski Ro5 pass (1.0) or fail (0.0) | — |
The result contains about 1,679 molecular descriptors in SDK 3.1.13 (BCUT,
GETAWAY, WHIM, etc.); access any via `result.descriptors["key"]`. For average
molecular weight, calculate it separately (for example, RDKit `MolWt`).
### 2. Microscopic pKa
For protonation-state energetics and acid/base behavior of a specific structure.
Four methods are available:
| Method | Input | Speed | Covers | Use when |
|--------|-------|-------|--------|----------|
| `chemprop_nevolianis2025` | SMILES string | Fast | Deprotonation only | Acidic groups only; quick screening |
| `starling` | SMILES string | Fast | Acid + base | Most drug-like molecules; preferred SMILES method |
| `aimnet2_wagen2024` | 3D molecule object | Slower | Acid + base | You already have a 3D structure |
| `gxtb_wagen2026` (**default**) | 3D molecule object | Slower | Acid + base | Current SDK default; set `method=` explicitly for reproducibility |
```python
# Fast path: SMILES input with full acid+base coverage (use starling method when available)
wf = rowan.submit_pka_workflow(
initial_molecule="c1ccccc1O", # phenol SMILES; param is initial_molecule, not initial_smiles
method="starling", # fast SMILES method, covers acid+base; chemprop_nevolianis2025 is deprotonation-only
name="phenol pKa",
)
result = wf.result()
print(result.strongest_acid) # 9.995 for phenol (verified; literature ~9.95)
print(result.strongest_base) # None when no basic site is found
print(result.conjugate_bases) # list of pKaMicrostate objects
# Access each microstate with .pka, .smiles, .atom_index, .delta_g, .uncertainty
```
### 3. MacropKa
For pH-dependent protonation behavior across a range.
```python
wf = rowan.submit_macropka_workflow(
initial_smiles="CN1CCN(CC1)C2=NC=NC3=CC=CC=C32", # imidazole
min_pH=0,
max_pH=14,
min_charge=-2, # default
max_charge=2, # default
compute_aqueous_solubility=True, # default
name="imidazole macropKa",
)
result = wf.result()
print(result.pka_values) # list of pKa values
print(result.logd_by_ph) # dict of {pH: logD}
print(result.aqueous_solubility_by_ph) # dict of {pH: solubility}
print(result.isoelectric_point) # isoelectric point
print(result.data)
# {'pKa_values': [...], 'logD_by_pH': {...}, 'aqueous_solubility_by_pH': {...}, ...}
```
### 4. Conformer search
For 3D ensemble generation when ensemble quality matters.
```python
wf = rowan.submit_conformer_search_workflow(
initial_molecule="CCOC(=O)N1CCC(CC1)Oc1ncnc2ccccc12",
name="conformer search",
)
result = wf.result()
print(result.num_conformers)
print(result.get_energies()) # [0.0, 1.2, 2.5, ...]
print(result.get_conformers()) # list of 3D molecules
print(result.get_conformer(0)) # lowest-energy conformer
# There is no num_conformers submit parameter. Configure the generator and
# ensemble through conf_gen_settings.
```
### 5. Tautomer search
For heterocycles and systems where tautomer state affects downstream modeling.
```python
wf = rowan.submit_tautomer_search_workflow(
initial_molecule=rowan.Molecule.from_smiles("O=c1[nH]ccnc1"),
name="imidazolone tautomers",
)
result = wf.result()
print(result.best_tautomer) # Most stable SMILES string
print(result.tautomers) # List of tautomeric SMILES
print(result.molecules) # List of molecule objects
```
### 6. Docking
For protein-ligand docking with optional pose refinement and conformer generation.
```python
# Upload protein once, reuse in multiple workflows
protein = rowan.upload_protein(
name="CDK2",
file_path="cdk2.pdb",
)
# Binding pocket: [[center_x, center_y, center_z], [size_x, size_y, size_z]] in Å
pocket = [[10.5, 24.2, 31.8], [18.0, 18.0, 18.0]]
# Submit docking
wf = rowan.submit_docking_workflow(
protein=protein,
pocket=pocket,
initial_molecule=rowan.Molecule.from_smiles(
"CCNc1ncc(c(Nc2ccc(F)cc2)n1)-c1cccnc1"
),
do_pose_refinement=True,
do_csearch=True,
name="lead docking",
)
result = wf.result()
print(result.scores) # Docking scores (kcal/mol)
print(result.best_pose) # Mol object with 3D coordinates
print(result.data) # Raw result dict
```
**Protein preparation tips:**
- PDB files should be reasonably clean (remove water/heteroatoms unless intended)
- Use the same protein object across a docking series for consistency
- If you have a PDB ID, use `rowan.create_protein_from_pdb_id()` instead
### 7. Analogue docking
For placing a compound series into a shared binding context.
```python
# Analogue series (e.g., SAR campaign)
analogues = [
"CCNc1ncc(c(Nc2ccc(F)cc2)n1)-c1cccnc1", # reference
"CCNc1ncc(c(Nc2ccc(Cl)cc2)n1)-c1cccnc1", # chloro
"CCNc1ncc(c(Nc2ccc(OC)cc2)n1)-c1cccnc1", # methoxy
"CCNc1ncc(c(Nc2cc(C)c(F)cc2)n1)-c1cccnc1", # methyl, fluoro
]
wf = rowan.submit_analogue_docking_workflow(
analogues=analogues,
initial_molecule=rowan.Molecule.from_smiles(analogues[0]), # reference ligand
protein=protein,
name="SAR series docking",
)
# Analogue docking does not accept a pocket parameter in SDK 3.1.13.
result = wf.result()
print(result.analogue_scores) # List of scores for each analogue
print(result.best_poses) # List of poses
```
### 8. MSA generation
For multiple-sequence alignment (useful for downstream cofolding).
```python
wf = rowan.submit_msa_workflow(
initial_protein_sequences=[
"MENFQKVEKIGEGTYGVVYKARNKLTGEVVALKKIRLDTETEGVP"
],
output_formats=["colabfold", "chai", "boltz"],
name="target MSA",
)
result = wf.result()
result.download_files() # Downloads alignments to disk
```
### 9. Protein-ligand cofolding
For AI-based bound-complex prediction when no crystal structure is available.
```python
wf = rowan.submit_protein_cofolding_workflow(
initial_protein_sequences=[
"MENFQKVEKIGEGTYGVVYKARNKLTGEVVALKKIRLDTETEGVP"
],
initial_smiles_list=[
"CCNc1ncc(c(Nc2ccc(F)cc2)n1)-c1cccnc1"
],
name="protein-ligand cofolding",
)
result = wf.result()
print(result.predictions) # List of predicted structures
print(result.messages) # Model metadata/warnings
predicted_structure = result.get_predicted_structure()
predicted_structure.write("predicted_complex.pdb")
```
## All supported workflow types
All workflows follow the same submit → wait → retrieve pattern and support webhooks and project/folder organization.
### Core molecular modeling workflows
| Workflow | Function | When to use |
|----------|----------|-------------|
| Descriptors | `submit_descriptors_workflow` | First-pass triage: MW, LogP, TPSA, HBA/HBD, Lipinski filter |
| pKa | `submit_pka_workflow` | Single ionizable group; need protonation thermodynamics |
| MacropKa | `submit_macropka_workflow` | Multi-ionizable drugs; pH-dependent charge/LogD/solubility |
| Conformer Search | `submit_conformer_search_workflow` | 3D ensemble for docking, MD, or SAR; known tautomer |
| Tautomer Search | `submit_tautomer_search_workflow` | Heterocycles, ketoenol; uncertain tautomeric form |
| Solubility | `submit_solubility_workflow` | Aqueous or solvent-specific solubility prediction |
| Membrane Permeability | `submit_membrane_permeability_workflow` | Caco-2, PAMPA, BBB, plasma permeability |
| ADMET | `submit_admet_workflow` | Broad drug-likeness and ADMET property sweep |
### Structure-based design workflows
| Workflow | Function | When to use |
|----------|----------|-------------|
| Docking | `submit_docking_workflow` | Single ligand, known binding pocket |
| Analogue Docking | `submit_analogue_docking_workflow` | SAR series (5100+ compounds) in a shared pocket |
| Batch Docking | `submit_batch_docking_workflow` | Fast library screening; large compound sets |
| Protein MD | `submit_protein_md_workflow` | Long-timescale dynamics; conformational sampling |
| Pose Analysis MD | `submit_pose_analysis_md_workflow` | MD refinement of a docking pose |
| Protein Cofolding | `submit_protein_cofolding_workflow` | No crystal structure; AI-predicted bound complex |
| Protein Binder Design | `submit_protein_binder_design_workflow` | De novo binder generation against a protein target |
### Advanced computational chemistry
| Workflow | Function | When to use |
|----------|----------|-------------|
| Basic Calculation | `submit_basic_calculation_workflow` | QM/ML geometry optimization or single-point energy |
| Electronic Properties | `submit_electronic_properties_workflow` | Dipole, partial charges, HOMO-LUMO, ESP |
| BDE | `submit_bde_workflow` | Bond dissociation energies; metabolic soft-spot prediction |
| Redox Potential | `submit_redox_potential_workflow` | Oxidation/reduction potentials |
| Spin States | `submit_spin_states_workflow` | Spin-state energy ordering for organometallics/radicals |
| Strain | `submit_strain_workflow` | Conformational strain relative to global minimum |
| Scan | `submit_scan_workflow` | PES scans; torsion profiles |
| Multistage Optimization | `submit_multistage_optimization_workflow` | Progressive optimization across levels of theory |
### Reaction chemistry
| Workflow | Function | When to use |
|----------|----------|-------------|
| Double-Ended TS Search | `submit_double_ended_ts_search_workflow` | Transition state between two known structures |
| IRC | `submit_irc_workflow` | Confirm TS connectivity; intrinsic reaction coordinate |
### Advanced properties
| Workflow | Function | When to use |
|----------|----------|-------------|
| NMR | `submit_nmr_workflow` | Predicted 1H/13C chemical shifts for structure verification |
| Ion Mobility | `submit_ion_mobility_workflow` | Collision cross-section (CCS) for MS method development |
| Hydrogen Bond Strength | `submit_hydrogen_bond_basicity_workflow` | H-bond donor/acceptor strength for formulation/solubility |
| Fukui | `submit_fukui_workflow` | Site reactivity indices for electrophilic/nucleophilic attack |
| Interaction Energy Decomposition | `submit_interaction_energy_decomposition_workflow` | Fragment-level interaction analysis |
### Binding free energy
| Workflow | Function | When to use |
|----------|----------|-------------|
| RBFE/FEP | `submit_relative_binding_free_energy_perturbation_workflow` | Relative ΔΔG for congeneric series |
| RBFE Graph | `submit_relative_binding_free_energy_graph_workflow` | Build and optimize an RBFE perturbation network |
### Sequence and structural biology
| Workflow | Function | When to use |
|----------|----------|-------------|
| MSA | `submit_msa_workflow` | Multiple sequence alignment for cofolding (ColabFold, Chai, Boltz) |
| Solvent-Dependent Conformers | `submit_solvent_dependent_conformers_workflow` | Solvation-aware conformer ensembles |
@@ -1,8 +1,8 @@
---
lineage_type: import
upstream_source: https://github.com/K-Dense-AI/scientific-agent-skills/blob/9c9bd2e9/skills/stable-baselines3/SKILL.md
upstream_sha: 9c9bd2e9
imported_at: 2026-06-27
upstream_source: https://github.com/K-Dense-AI/scientific-agent-skills/blob/72d742e1/skills/stable-baselines3/SKILL.md
upstream_sha: 72d742e1
imported_at: 2026-08-29
prompt_class: unknown
upstream_changes: accepted
name: stable-baselines3
@@ -10,7 +10,9 @@ description: Production-ready reinforcement learning algorithms (PPO, SAC, DQN,
license: MIT license
allowed-tools: Read Write Edit Bash
compatibility: Requires Python 3.10+, PyTorch >= 2.3, and stable-baselines3 2.8+. Gymnasium environments; optional extras for TensorBoard and Atari (ale-py).
metadata: {"version": "1.1", "skill-author": "K-Dense Inc."}
metadata:
version: "1.2"
skill-author: K-Dense Inc.
---
# Stable Baselines3
@@ -1,16 +1,24 @@
---
lineage_type: import
upstream_source: https://github.com/K-Dense-AI/scientific-agent-skills/blob/9c9bd2e9/skills/rowan/SKILL.md
upstream_sha: 9c9bd2e9
imported_at: 2026-06-27
upstream_source: https://github.com/K-Dense-AI/scientific-agent-skills/blob/72d742e1/skills/rowan/SKILL.md
upstream_sha: 72d742e1
imported_at: 2026-08-29
prompt_class: prompt
upstream_changes: accepted
name: rowan
description: Rowan is a cloud-native molecular modeling and medicinal-chemistry workflow platform with a Python API. Use for pKa and macropKa prediction, conformer and tautomer ensembles, docking and analogue docking, protein-ligand cofolding, MSA generation, molecular dynamics, permeability, descriptor workflows, and related small-molecule or protein modeling tasks. Ideal for programmatic batch screening, multi-step chemistry pipelines, and workflows that would otherwise require maintaining local HPC/GPU infrastructure.
license: Proprietary (API key required)
compatibility: Python 3.12+, API key required
required_environment_variables: [{"name": "ROWAN_API_KEY", "prompt": "Rowan computational chemistry API key.", "required_for": "full functionality"}]
metadata: {"version": "1.2", "skill-author": "Rowan Science", "trigger-keywords": "pKa prediction, molecular docking, conformer search, chemistry workflow, drug discovery, SMILES, protein structure, batch molecular modeling, cloud chemistry", "openclaw": {"primaryEnv": "ROWAN_API_KEY", "envVars": [{"name": "ROWAN_API_KEY", "required": true, "description": "Rowan computational chemistry API key."}]}}
metadata:
version: "1.5"
skill-author: Rowan Science
trigger-keywords: pKa prediction, molecular docking, conformer search, chemistry workflow, drug discovery, SMILES, protein structure, batch molecular modeling, cloud chemistry
openclaw:
primaryEnv: ROWAN_API_KEY
envVars:
- name: ROWAN_API_KEY
required: true
description: Rowan computational chemistry API key.
---
# Rowan: Cloud-Native Molecular-Modeling and Drug-Design Workflows
@@ -37,40 +45,6 @@ Use Rowan when you want to run medicinal-chemistry or molecular-design workflows
- Simple molecular I/O (use RDKit directly)
- Post-HF *ab initio* quantum chemistry or relativistic calculations
## Access and pricing model
Rowan uses a credit-based usage model. All users, including free-tier users, can create API keys and use the Python API.
### Free-tier access
- Access to all Rowan core workflows
- 20 credits per week
- 500 signup credits
### Pricing and credit consumption
Credits are consumed according to compute type:
- **CPU**: 1 credit per minute
- **GPU**: 3 credits per minute
- **H100/H200 GPU**: 7 credits per minute
Purchased credits are priced per credit and remain valid for up to one year from purchase.
### Typical cost estimates
| Workflow | Typical Runtime | Estimated Credits | Notes |
|----------|----------------|-------------------|-------|
| Descriptors | <1 min | 0.52 | Lightweight, good for triage |
| pKa (single transition) | 25 min | 25 | Depends on molecule size |
| MacropKa (pH 014) | 515 min | 515 | Broader sampling, higher cost |
| Conformer search | 310 min | 310 | Ensemble quality matters |
| Tautomer search | 25 min | 25 | Heterocyclic systems |
| Docking (single ligand) | 520 min | 520 | Depends on pocket size, refinement |
| Analogue docking series (1050 ligands) | 30120 min | 30100+ | Shared reference frame |
| MSA generation | 530 min | 530 | Sequence length dependent |
| Protein-ligand cofolding | 1560 min | 2050+ | AI structure prediction, GPU-heavy |
## Quick start
```bash
@@ -81,22 +55,24 @@ uv pip install rowan-python
import rowan
rowan.api_key = "your_api_key_here" # or set ROWAN_API_KEY env var
# Submit a descriptors workflow — completes in under a minute
wf = rowan.submit_descriptors_workflow("CC(=O)Oc1ccccc1C(=O)O", name="aspirin")
# Descriptors require a 3D Molecule, not a bare SMILES string.
mol = rowan.Molecule.from_smiles("CC(=O)Oc1ccccc1C(=O)O")
wf = rowan.submit_descriptors_workflow(mol, name="aspirin")
result = wf.result()
print(result.descriptors['MW']) # 180.16
print(result.descriptors['SLogP']) # 1.19
print(result.descriptors['TPSA']) # 59.44
print(result.descriptors["MW"]) # 180.042 — exact mass
print(result.descriptors["SLogP"]) # 1.31
print(result.descriptors["TopoPSA"]) # 63.6 — topological PSA
```
If that prints without error, you're set up correctly.
If that prints without error, you're set up correctly. These values and examples
were verified against `rowan-python` 3.1.13.
## Installation
```bash
uv pip install rowan-python
# or: pip install rowan-python
# or: uv pip install rowan-python
```
## User and webhook management
@@ -122,33 +98,7 @@ Verify authentication:
import rowan
user = rowan.whoami() # Returns user info if authenticated
print(f"User: {user.email}")
print(f"Credits available: {user.credits_available_string}")
```
### Webhook secret management
For webhook signature verification, manage secrets through your user account:
```python
import rowan
# Get your current webhook secret (returns None if none exists)
secret = rowan.get_webhook_secret()
if secret is None:
secret = rowan.create_webhook_secret()
print(f"Secret key: {secret.secret}")
# Rotate your secret (invalidates old, creates new)
# Use this periodically for security
new_secret = rowan.rotate_webhook_secret()
print(f"New secret created (old secret disabled): {new_secret.secret}")
# Verify incoming webhook signatures
is_valid = rowan.verify_webhook_secret(
request_body=b"...", # Raw request body (bytes)
signature="X-Rowan-Signature", # From request header
secret=secret.secret
)
print(f"Credits available: {user.credits_available_string()}")
```
## Molecule input formats
@@ -159,7 +109,18 @@ Rowan accepts molecules in the following formats:
- **SMARTS patterns** (for some workflows): subset of SMARTS for substructure matching
- **InChI** (if supported in your API version): `"InChI=1S/C2H6O/c1-2-3/h3H,2H2,1H3"`
The API will validate input and raise a `rowan.ValidationError` if a molecule cannot be parsed. Always use canonicalized SMILES for reproducibility.
The API validates molecule inputs and raises `ValueError` for an unparseable
SMILES or a workflow-incompatible input type. Always use canonicalized SMILES
for reproducibility.
### SMILES strings versus molecule objects
Accepted input types vary by workflow in `rowan-python` 3.1.13. Only these
common workflows accept a bare string: pKa, conformer search, membrane
permeability, ADMET, LogP, macropKa, solubility, and pose-analysis MD. Most
others — including descriptors, tautomer search, docking, analogue docking,
BDE, NMR, and Fukui — require `rowan.Molecule.from_smiles(smiles)` or an RDKit
`Mol`/`RWMol`. A wrong type raises `ValueError` before submission.
**Tip:** Use RDKit to validate SMILES before submission:
@@ -184,21 +145,21 @@ import rowan
# 1. Submit — use the specific workflow function (not the generic submit_workflow)
workflow = rowan.submit_descriptors_workflow(
"CC(=O)Oc1ccccc1C(=O)O",
rowan.Molecule.from_smiles("CC(=O)Oc1ccccc1C(=O)O"),
name="aspirin descriptors",
)
# 2. & 3. Wait and retrieve
result = workflow.result() # Blocks until done (default: wait=True, poll_interval=5)
print(result.data) # Raw dict
print(result.descriptors['MW']) # 180.16 — use result.descriptors dict, not result.molecular_weight
print(result.descriptors["MW"]) # 180.042 exact mass; no result.molecular_weight property
```
For long-running workflows, use streaming:
```python
for partial in workflow.stream_result(poll_interval=5):
print(f"Progress: {partial.complete}%")
print(f"Complete: {partial.complete}") # bool, not a percentage
print(partial.data)
```
@@ -219,28 +180,30 @@ Rowan's API includes **typed workflow result objects** with convenience properti
Results have two access patterns:
1. **Convenience properties** (recommended first): `result.descriptors`, `result.best_pose`, `result.conformer_energies`
1. **Convenience properties** (recommended first): `result.descriptors`, `result.best_pose`, `result.scores`. Result classes differ: conformer search uses `get_energies()` and `get_conformers()` methods.
2. **Raw fallback**: `result.data` — raw dictionary from the API
Example:
```python
result = rowan.submit_descriptors_workflow(
"CCO",
rowan.Molecule.from_smiles("CCO"),
name="ethanol",
).result()
# Convenience property (returns dict of all descriptors):
print(result.descriptors['MW']) # 46.042
print(result.descriptors['SLogP']) # -0.001
print(result.descriptors['TPSA']) # 57.96
# Convenience property (returns all descriptors):
print(result.descriptors["MW"]) # exact/monoisotopic mass
print(result.descriptors["SLogP"])
print(result.descriptors["TopoPSA"]) # usual topological PSA
# Raw data fallback (descriptors are nested under 'descriptors' key):
print(result.data['descriptors'])
# {'MW': 46.042, 'SLogP': -0.001, 'TPSA': 57.96, 'nHBDon': 1.0, 'nHBAcc': 1.0, ...}
# Raw data fallback:
print(result.data["descriptors"])
```
**Note:** `DescriptorsResult` does **not** have a `molecular_weight` property. Descriptor keys use short names (`MW`, `SLogP`, `nHBDon`) not verbose names.
**Note:** `DescriptorsResult` does **not** have a `molecular_weight` property.
`MW` is exact/monoisotopic mass, not average molecular weight. `TPSA` is a 3D
charged-surface descriptor; use `TopoPSA` for the usual topological polar
surface area used in drug-likeness rules.
### Cache invalidation
@@ -248,7 +211,7 @@ Some result properties are lazily loaded (e.g., conformer geometries, protein st
```python
result.clear_cache()
new_structures = result.conformer_molecules # Refetched
new_structures = result.get_conformers() # Refetched for ConformerSearchResult
```
## Projects, folders, and organization
@@ -265,11 +228,13 @@ project = rowan.create_project(name="CDK2 lead optimization")
rowan.set_project("CDK2 lead optimization")
# All subsequent workflows go into this project
wf = rowan.submit_descriptors_workflow("CCO", name="test compound")
wf = rowan.submit_descriptors_workflow(
rowan.Molecule.from_smiles("CCO"), name="test compound"
)
# Retrieve later
project = rowan.retrieve_project("CDK2 lead optimization")
workflows = rowan.list_workflows(project=project, size=50)
# retrieve_project takes a UUID; list_workflows scopes with parent_uuid.
project = rowan.retrieve_project(project.uuid)
workflows = rowan.list_workflows(parent_uuid=project.uuid, size=50)
```
### Folders
@@ -285,7 +250,7 @@ wf = rowan.submit_docking_workflow(
)
# List workflows in a folder
results = rowan.list_workflows(folder=folder)
results = rowan.list_workflows(parent_uuid=folder.uuid)
```
## Workflow decision trees
@@ -334,7 +299,7 @@ ADME assessment across GI pH: Use macropKa
```python
# Step 1: Find best tautomer
taut_wf = rowan.submit_tautomer_search_workflow(
initial_molecule="O=c1[nH]ccnc1",
initial_molecule=rowan.Molecule.from_smiles("O=c1[nH]ccnc1"),
name="imidazole tautomers",
)
best_taut = taut_wf.result().best_tautomer
@@ -354,528 +319,6 @@ conf_wf = rowan.submit_conformer_search_workflow(
| Analogue docking | 5100+ related compounds | Protein + SMILES list + reference ligand | All poses, reference-aligned |
| Protein-ligand cofolding | Sequence + ligand, no crystal structure | Protein sequence + SMILES | ML-predicted bound complex |
## Common workflow categories
### 1. Descriptors
A lightweight entry point for batch triage, SAR, or exploratory scripts.
```python
wf = rowan.submit_descriptors_workflow(
"CC(=O)Oc1ccccc1C(=O)O", # positional arg, accepts SMILES string
name="aspirin descriptors",
)
result = wf.result()
print(result.descriptors['MW']) # 180.16
print(result.descriptors['SLogP']) # 1.19
print(result.descriptors['TPSA']) # 59.44
print(result.data['descriptors'])
# {'MW': 180.16, 'SLogP': 1.19, 'TPSA': 59.44, 'nHBDon': 1.0, 'nHBAcc': 4.0, ...}
```
**Common descriptor keys:**
| Key | Description | Typical drug range |
|-----|-------------|-------------------|
| `MW` | Molecular weight (Da) | <500 (Lipinski) |
| `SLogP` | Calculated LogP (lipophilicity) | -2 to +5 |
| `TPSA` | Topological polar surface area (Ų) | <140 for oral bioavailability |
| `nHBDon` | H-bond donor count | ≤5 (Lipinski) |
| `nHBAcc` | H-bond acceptor count | ≤10 (Lipinski) |
| `nRot` | Rotatable bond count | <10 for oral drugs |
| `nRing` | Ring count | — |
| `nHeavyAtom` | Heavy atom count | — |
| `FilterItLogS` | Estimated aqueous solubility (LogS) | >-4 preferred |
| `Lipinski` | Lipinski Ro5 pass (1.0) or fail (0.0) | — |
The result contains hundreds of additional molecular descriptors (BCUT, GETAWAY, WHIM, etc.); access any via `result.descriptors['key']`.
### 2. Microscopic pKa
For protonation-state energetics and acid/base behavior of a specific structure.
Two methods are available:
| Method | Input | Speed | Covers | Use when |
|--------|-------|-------|--------|----------|
| `chemprop_nevolianis2025` | SMILES string | Fast | Deprotonation only (anionic conjugate bases) | Acidic groups only; quick screening |
| `starling` | SMILES string | Fast | Acid + base (full protonation/deprotonation) | Most drug-like molecules; preferred SMILES method |
| `aimnet2_wagen2024` (default) | 3D molecule object | Slower, higher accuracy | Acid + base | You already have a 3D structure (e.g. from conformer search) |
```python
# Fast path: SMILES input with full acid+base coverage (use starling method when available)
wf = rowan.submit_pka_workflow(
initial_molecule="c1ccccc1O", # phenol SMILES; param is initial_molecule, not initial_smiles
method="starling", # fast SMILES method, covers acid+base; chemprop_nevolianis2025 is deprotonation-only
name="phenol pKa",
)
result = wf.result()
print(result.strongest_acid) # 9.81 (pKa of the most acidic site)
print(result.conjugate_bases) # list of {pka, smiles, atom_index, ...} per deprotonatable site
```
### 3. MacropKa
For pH-dependent protonation behavior across a range.
```python
wf = rowan.submit_macropka_workflow(
initial_smiles="CN1CCN(CC1)C2=NC=NC3=CC=CC=C32", # imidazole
min_pH=0,
max_pH=14,
min_charge=-2, # default
max_charge=2, # default
compute_aqueous_solubility=True, # default
name="imidazole macropKa",
)
result = wf.result()
print(result.pka_values) # list of pKa values
print(result.logd_by_ph) # dict of {pH: logD}
print(result.aqueous_solubility_by_ph) # dict of {pH: solubility}
print(result.isoelectric_point) # isoelectric point
print(result.data)
# {'pKa_values': [...], 'logD_by_pH': {...}, 'aqueous_solubility_by_pH': {...}, ...}
```
### 4. Conformer search
For 3D ensemble generation when ensemble quality matters.
```python
wf = rowan.submit_conformer_search_workflow(
initial_molecule="CCOC(=O)N1CCC(CC1)Oc1ncnc2ccccc12",
num_conformers=50, # Optional: override default
name="conformer search",
)
result = wf.result()
print(result.conformer_energies) # [0.0, 1.2, 2.5, ...]
print(result.conformer_molecules) # List of 3D molecules
print(result.best_conformer) # Lowest-energy conformer
```
### 5. Tautomer search
For heterocycles and systems where tautomer state affects downstream modeling.
```python
wf = rowan.submit_tautomer_search_workflow(
initial_molecule="O=c1[nH]ccnc1", # or keto tautomer
name="imidazolone tautomers",
)
result = wf.result()
print(result.best_tautomer) # Most stable SMILES string
print(result.tautomers) # List of tautomeric SMILES
print(result.molecules) # List of molecule objects
```
### 6. Docking
For protein-ligand docking with optional pose refinement and conformer generation.
```python
# Upload protein once, reuse in multiple workflows
protein = rowan.upload_protein(
name="CDK2",
file_path="cdk2.pdb",
)
# Define binding pocket
pocket = {
"center": [10.5, 24.2, 31.8],
"size": [18.0, 18.0, 18.0],
}
# Submit docking
wf = rowan.submit_docking_workflow(
protein=protein,
pocket=pocket,
initial_molecule="CCNc1ncc(c(Nc2ccc(F)cc2)n1)-c1cccnc1",
do_pose_refinement=True,
do_conformer_search=True,
name="lead docking",
)
result = wf.result()
print(result.scores) # Docking scores (kcal/mol)
print(result.best_pose) # Mol object with 3D coordinates
print(result.data) # Raw result dict
```
**Protein preparation tips:**
- PDB files should be reasonably clean (remove water/heteroatoms unless intended)
- Use the same protein object across a docking series for consistency
- If you have a PDB ID, use `rowan.create_protein_from_pdb_id()` instead
### 7. Analogue docking
For placing a compound series into a shared binding context.
```python
# Analogue series (e.g., SAR campaign)
analogues = [
"CCNc1ncc(c(Nc2ccc(F)cc2)n1)-c1cccnc1", # reference
"CCNc1ncc(c(Nc2ccc(Cl)cc2)n1)-c1cccnc1", # chloro
"CCNc1ncc(c(Nc2ccc(OC)cc2)n1)-c1cccnc1", # methoxy
"CCNc1ncc(c(Nc2cc(C)c(F)cc2)n1)-c1cccnc1", # methyl, fluoro
]
wf = rowan.submit_analogue_docking_workflow(
analogues=analogues,
initial_molecule=analogues[0], # Reference ligand
protein=protein,
pocket=pocket,
name="SAR series docking",
)
result = wf.result()
print(result.analogue_scores) # List of scores for each analogue
print(result.best_poses) # List of poses
```
### 8. MSA generation
For multiple-sequence alignment (useful for downstream cofolding).
```python
wf = rowan.submit_msa_workflow(
initial_protein_sequences=[
"MENFQKVEKIGEGTYGVVYKARNKLTGEVVALKKIRLDTETEGVP"
],
output_formats=["colabfold", "chai", "boltz"],
name="target MSA",
)
result = wf.result()
result.download_files() # Downloads alignments to disk
```
### 9. Protein-ligand cofolding
For AI-based bound-complex prediction when no crystal structure is available.
```python
wf = rowan.submit_protein_cofolding_workflow(
initial_protein_sequences=[
"MENFQKVEKIGEGTYGVVYKARNKLTGEVVALKKIRLDTETEGVP"
],
initial_smiles_list=[
"CCNc1ncc(c(Nc2ccc(F)cc2)n1)-c1cccnc1"
],
name="protein-ligand cofolding",
)
result = wf.result()
print(result.predictions) # List of predicted structures
print(result.messages) # Model metadata/warnings
predicted_structure = result.get_predicted_structure()
predicted_structure.write("predicted_complex.pdb")
```
## All supported workflow types
All workflows follow the same submit → wait → retrieve pattern and support webhooks and project/folder organization.
### Core molecular modeling workflows
| Workflow | Function | When to use |
|----------|----------|-------------|
| Descriptors | `submit_descriptors_workflow` | First-pass triage: MW, LogP, TPSA, HBA/HBD, Lipinski filter |
| pKa | `submit_pka_workflow` | Single ionizable group; need protonation thermodynamics |
| MacropKa | `submit_macropka_workflow` | Multi-ionizable drugs; pH-dependent charge/LogD/solubility |
| Conformer Search | `submit_conformer_search_workflow` | 3D ensemble for docking, MD, or SAR; known tautomer |
| Tautomer Search | `submit_tautomer_search_workflow` | Heterocycles, ketoenol; uncertain tautomeric form |
| Solubility | `submit_solubility_workflow` | Aqueous or solvent-specific solubility prediction |
| Membrane Permeability | `submit_membrane_permeability_workflow` | Caco-2, PAMPA, BBB, plasma permeability |
| ADMET | `submit_admet_workflow` | Broad drug-likeness and ADMET property sweep |
### Structure-based design workflows
| Workflow | Function | When to use |
|----------|----------|-------------|
| Docking | `submit_docking_workflow` | Single ligand, known binding pocket |
| Analogue Docking | `submit_analogue_docking_workflow` | SAR series (5100+ compounds) in a shared pocket |
| Batch Docking | `submit_batch_docking_workflow` | Fast library screening; large compound sets |
| Protein MD | `submit_protein_md_workflow` | Long-timescale dynamics; conformational sampling |
| Pose Analysis MD | `submit_pose_analysis_md_workflow` | MD refinement of a docking pose |
| Protein Cofolding | `submit_protein_cofolding_workflow` | No crystal structure; AI-predicted bound complex |
| Protein Binder Design | `submit_protein_binder_design_workflow` | De novo binder generation against a protein target |
### Advanced computational chemistry
| Workflow | Function | When to use |
|----------|----------|-------------|
| Basic Calculation | `submit_basic_calculation_workflow` | QM/ML geometry optimization or single-point energy |
| Electronic Properties | `submit_electronic_properties_workflow` | Dipole, partial charges, HOMO-LUMO, ESP |
| BDE | `submit_bde_workflow` | Bond dissociation energies; metabolic soft-spot prediction |
| Redox Potential | `submit_redox_potential_workflow` | Oxidation/reduction potentials |
| Spin States | `submit_spin_states_workflow` | Spin-state energy ordering for organometallics/radicals |
| Strain | `submit_strain_workflow` | Conformational strain relative to global minimum |
| Scan | `submit_scan_workflow` | PES scans; torsion profiles |
| Multistage Optimization | `submit_multistage_opt_workflow` | Progressive optimization across levels of theory |
### Reaction chemistry
| Workflow | Function | When to use |
|----------|----------|-------------|
| Double-Ended TS Search | `submit_double_ended_ts_search_workflow` | Transition state between two known structures |
| IRC | `submit_irc_workflow` | Confirm TS connectivity; intrinsic reaction coordinate |
### Advanced properties
| Workflow | Function | When to use |
|----------|----------|-------------|
| NMR | `submit_nmr_workflow` | Predicted 1H/13C chemical shifts for structure verification |
| Ion Mobility | `submit_ion_mobility_workflow` | Collision cross-section (CCS) for MS method development |
| Hydrogen Bond Strength | `submit_hydrogen_bond_basicity_workflow` | H-bond donor/acceptor strength for formulation/solubility |
| Fukui | `submit_fukui_workflow` | Site reactivity indices for electrophilic/nucleophilic attack |
| Interaction Energy Decomposition | `submit_interaction_energy_decomposition_workflow` | Fragment-level interaction analysis |
### Binding free energy
| Workflow | Function | When to use |
|----------|----------|-------------|
| RBFE/FEP | `submit_relative_binding_free_energy_perturbation_workflow` | Relative ΔΔG for congeneric series |
| RBFE Graph | `submit_rbfe_graph_workflow` | Build and optimize an RBFE perturbation network |
### Sequence and structural biology
| Workflow | Function | When to use |
|----------|----------|-------------|
| MSA | `submit_msa_workflow` | Multiple sequence alignment for cofolding (ColabFold, Chai, Boltz) |
| Solvent-Dependent Conformers | `submit_solvent_dependent_conformers_workflow` | Solvation-aware conformer ensembles |
## Batch submission and retrieval
For libraries or analogue series, submit in a loop using the specific workflow function. The generic `rowan.batch_submit_workflow()` and `rowan.submit_workflow()` functions currently return 422 errors from the API — use the named functions (`submit_descriptors_workflow`, `submit_pka_workflow`, etc.) instead.
### Submit a batch
```python
smileses = ["CCO", "CC(=O)O", "c1ccccc1O"]
names = ["ethanol", "acetic acid", "phenol"]
workflows = [
rowan.submit_descriptors_workflow(smi, name=name)
for smi, name in zip(smileses, names)
]
print(f"Submitted {len(workflows)} workflows")
```
### Poll batch status
```python
statuses = rowan.batch_poll_status([wf.uuid for wf in workflows])
# Returns aggregate counts — not per-UUID:
# {'queued': 0, 'running': 1, 'complete': 2, 'failed': 0, 'total': 3, ...}
if statuses["complete"] == statuses["total"]:
print("All workflows done")
elif statuses["failed"] > 0:
print(f"{statuses['failed']} workflows failed")
```
### Retrieve and collect results
```python
results = []
for wf in workflows:
try:
result = wf.result()
results.append(result.data)
except rowan.WorkflowError as e:
print(f"Workflow {wf.uuid} failed: {e}")
# Optionally aggregate into DataFrame
import pandas as pd
df = pd.DataFrame(results)
```
### Non-blocking / fire-and-check pattern
For long-running workflows where you don't want to hold a process open, submit workflows, save their UUIDs, and check back later in a separate process.
**Session 1 — submit and save UUIDs:**
```python
import rowan, json
rowan.api_key = "..."
smileses = ["CCO", "CC(=O)O", "c1ccccc1O"]
workflows = [
rowan.submit_descriptors_workflow(smi, name=f"compound_{i}")
for i, smi in enumerate(smileses)
]
# Save UUIDs to disk (or a database)
uuids = [wf.uuid for wf in workflows]
with open("workflow_uuids.json", "w") as f:
json.dump(uuids, f)
print("Submitted. Check back later.")
```
**Session 2 — check status and collect results when ready:**
```python
import rowan, json
rowan.api_key = "..."
with open("workflow_uuids.json") as f:
uuids = json.load(f)
results = []
for uuid in uuids:
wf = rowan.retrieve_workflow(uuid)
if wf.done():
result = wf.result(wait=False)
results.append({"uuid": uuid, "data": result.data})
else:
print(f"{uuid}: still running ({wf.status})")
print(f"Collected {len(results)} completed results")
```
## Webhooks and asynchronous workflows
For long-running campaigns or when you don't want to keep a process alive, use webhooks to notify your backend when workflows complete.
### Setting up webhooks
Every workflow submission function accepts a `webhook_url` parameter:
```python
wf = rowan.submit_docking_workflow(
protein=protein,
pocket=pocket,
initial_molecule="CCO",
webhook_url="https://myserver.com/rowan_callback",
name="docking with webhook",
)
print(f"Workflow submitted. Result will be POSTed to webhook when complete.")
```
Webhook URLs can be passed to any specific workflow function (`submit_docking_workflow()`, `submit_pka_workflow()`, `submit_descriptors_workflow()`, etc.).
### Webhook authentication with secrets
Rowan supports webhook signature verification to ensure requests are authentic. You'll need to:
1. **Create or retrieve a webhook secret:**
```python
import rowan
# Create a new webhook secret
secret = rowan.create_webhook_secret()
print(f"Your webhook secret: {secret.secret}")
# Or retrieve an existing secret
secret = rowan.get_webhook_secret()
# Rotate your secret (invalidates old one, creates new)
new_secret = rowan.rotate_webhook_secret()
```
2. **Verify incoming webhook requests:**
```python
import rowan
import hmac
import json
def verify_webhook(request_body: bytes, signature: str, secret: str) -> bool:
"""Verify the HMAC-SHA256 signature of a webhook request."""
return rowan.verify_webhook_secret(request_body, signature, secret)
```
### Webhook payload and signature
When a workflow completes, Rowan POSTs a JSON payload to your webhook URL with the header:
```text
X-Rowan-Signature: <HMAC-SHA256 signature>
```
The request body contains the complete workflow result:
```json
{
"workflow_uuid": "wf_12345abc",
"workflow_type": "docking",
"workflow_name": "lead docking",
"status": "COMPLETED_OK",
"created_at": "2025-04-01T12:00:00Z",
"completed_at": "2025-04-01T12:15:30Z",
"data": {
"scores": [-8.2, -8.0, -7.9],
"best_pose": {...},
"metadata": {...}
}
}
```
### Example webhook handler with signature verification (FastAPI)
```python
from fastapi import FastAPI, Request, HTTPException
import rowan
import json
app = FastAPI()
_ws = rowan.get_webhook_secret() or rowan.create_webhook_secret()
webhook_secret = _ws.secret
@app.post("/rowan_callback")
async def handle_rowan_webhook(request: Request):
# Get request body and signature
body = await request.body()
signature = request.headers.get("X-Rowan-Signature")
if not signature:
raise HTTPException(status_code=400, detail="Missing X-Rowan-Signature header")
# Verify signature
if not rowan.verify_webhook_secret(body, signature, webhook_secret):
raise HTTPException(status_code=401, detail="Invalid webhook signature")
# Parse and process
payload = json.loads(body)
wf_uuid = payload["workflow_uuid"]
status = payload["status"]
if status == "COMPLETED_OK":
print(f"Workflow {wf_uuid} succeeded!")
result_data = payload["data"]
# Process result, update database, trigger next workflow, etc.
elif status == "FAILED":
print(f"Workflow {wf_uuid} failed!")
# Handle failure
# Respond quickly to prevent retries
return {"status": "received"}
```
### Webhook best practices
- **Always verify signatures** using `rowan.verify_webhook_secret()` to ensure requests are from Rowan
- **Respond quickly** (< 5 seconds); offload heavy processing to async tasks or background jobs
- **Implement idempotency**: workflows may retry; handle duplicate payloads gracefully using `workflow_uuid`
- **Log all events** for debugging and audit trails
- **Use for long campaigns**: webhooks shine with 50+ workflows; for small jobs, polling with `result()` is simpler
- **Rotate secrets regularly** using `rowan.rotate_webhook_secret()` for security
- **Return 2xx status** to confirm receipt; Rowan may retry on 5xx errors
## Protein utilities
### Upload proteins
@@ -909,167 +352,36 @@ my_proteins = rowan.list_proteins()
- **Resolution**: Works with NMR structures, homology models, and cryo-EM; quality matters for downstream predictions
- **Validation**: Rowan validates PDB syntax; severely malformed files may be rejected
## End-to-end example: Lead optimization campaign
## Workflow catalog
This example demonstrates a realistic workflow for optimizing a hit compound:
Nine common workflow categories — descriptors, microscopic pKa, MacropKa, conformer
search, tautomer search, docking, analogue docking, MSA generation, and protein-ligand
cofolding — each with submission code and result shapes, plus the complete list of every
supported workflow type (core modeling, structure-based design, advanced computational
chemistry, reaction chemistry, advanced properties, binding free energy, and sequence and
structural biology) are in
[references/workflow_catalog.md](references/workflow_catalog.md).
```python
import rowan
import pandas as pd
## Batch submission, webhooks, and asynchronous work
# 1. Create a project and folder for organization
project = rowan.create_project(name="CDK2 Hit Optimization")
rowan.set_project("CDK2 Hit Optimization")
folder = rowan.create_folder(name="round_1_tautomers_and_pka")
Batch submit/poll/retrieve, the non-blocking fire-and-check pattern, webhook setup,
secret creation and rotation, payload and signature verification (with a FastAPI
handler), and webhook best practices are in
[references/batch_and_webhooks.md](references/batch_and_webhooks.md).
# 2. Load hit compound and analogues
hit = "CCNc1ncc(c(Nc2ccc(F)cc2)n1)-c1cccnc1" # Known hit
analogues = [
"CCNc1ncc(c(Nc2ccccc2)n1)-c1cccnc1", # Remove F
"CCNc1ncc(c(Nc2ccc(Cl)cc2)n1)-c1cccnc1", # Cl instead of F
"CCC(C)Nc1ncc(c(Nc2ccc(F)cc2)n1)-c1cccnc1", # Propyl instead of ethyl
]
## Access, pricing, and credits
# 3. Determine best tautomers (just in case)
print("Searching tautomeric forms...")
taut_workflows = [
rowan.submit_tautomer_search_workflow(
smi, name=f"analog_{i}", folder=folder,
)
for i, smi in enumerate(analogues)
]
Free-tier limits, credit consumption per workflow, and typical cost estimates are in
[references/access_and_pricing.md](references/access_and_pricing.md).
best_tautomers = []
for wf in taut_workflows:
result = wf.result()
best_tautomers.append(result.best_tautomer)
## Worked example and troubleshooting
# 4. Predict pKa and basic properties for all analogues
print("Predicting pKa and properties...")
pka_workflows = [
rowan.submit_pka_workflow(
smi, method="chemprop_nevolianis2025", name=f"pka_{i}", folder=folder,
)
for i, smi in enumerate(best_tautomers)
]
A full lead-optimization campaign — project setup, tautomers, pKa across an analogue
series, result collection, and a docking follow-up — is in
[references/end_to_end_example.md](references/end_to_end_example.md).
descriptor_workflows = [
rowan.submit_descriptors_workflow(smi, name=f"desc_{i}", folder=folder)
for i, smi in enumerate(best_tautomers)
]
# 5. Collect results
pka_results = []
for wf in pka_workflows:
try:
result = wf.result()
pka_results.append({
"compound": wf.name,
"pka": result.strongest_acid, # pKa of the strongest acid site
"uuid": wf.uuid,
})
except rowan.WorkflowError as e:
print(f"pKa prediction failed for {wf.name}: {e}")
descriptor_results = []
for wf in descriptor_workflows:
try:
result = wf.result()
desc = result.descriptors
descriptor_results.append({
"compound": wf.name,
"mw": desc.get("MW"),
"logp": desc.get("SLogP"),
"hba": desc.get("nHBAcc"),
"hbd": desc.get("nHBDon"),
"uuid": wf.uuid,
})
except rowan.WorkflowError as e:
print(f"Descriptor calculation failed for {wf.name}: {e}")
# 6. Merge and summarize
df_pka = pd.DataFrame(pka_results)
df_desc = pd.DataFrame(descriptor_results)
df = df_pka.merge(df_desc, on="compound", how="outer")
print("\n=== Preliminary SAR ===")
print(df.to_string())
# 7. Select promising compound for docking
# compound names are "pka_0", "pka_1", etc. — extract index to look up SMILES
top_idx = int(df.loc[df["pka"].idxmin(), "compound"].split("_")[1])
top_smiles = best_tautomers[top_idx]
print(f"\nProceeding with docking: {top_smiles}")
# 8. Docking campaign
protein = rowan.create_protein_from_pdb_id(name="CDK2_1CKP", code="1CKP")
pocket = {"center": [10.5, 24.2, 31.8], "size": [18.0, 18.0, 18.0]}
docking_wf = rowan.submit_docking_workflow(
protein=protein,
pocket=pocket,
initial_molecule=top_smiles,
do_pose_refinement=True,
name=f"docking_{top_compound}",
)
dock_result = docking_wf.result()
print(f"\nDocking score: {dock_result.scores[0]:.2f} kcal/mol")
print(f"Best pose saved to: best_pose.pdb")
dock_result.best_pose.write("best_pose.pdb")
```
## Error handling and troubleshooting
### Common errors and solutions
```python
import rowan
# Error 1: Invalid SMILES
try:
wf = rowan.submit_descriptors_workflow("CCCC(CC", name="bad smiles") # Invalid
except rowan.ValidationError as e:
print(f"Invalid SMILES: {e}")
# Solution: Use RDKit to validate before submission
from rdkit import Chem
smi = Chem.MolToSmiles(Chem.MolFromSmiles(smi))
# Error 2: API key not set
try:
wf = rowan.submit_descriptors_workflow("CCO")
except rowan.AuthenticationError:
print("API key not found. Set ROWAN_API_KEY env var or call rowan.api_key = '...'")
# Error 3: Insufficient credits
try:
wf = rowan.submit_protein_cofolding_workflow(...)
except rowan.InsufficientCreditsError as e:
print(f"Not enough credits: {e}. Purchase more or reduce job size.")
# Error 4: Workflow failed (bad molecule, etc.)
try:
wf = rowan.submit_docking_workflow(...)
result = wf.result()
except rowan.WorkflowError as e:
print(f"Workflow failed: {e}")
# Check wf.status for details
print(f"Status: {wf.status}")
# Error 5: Workflow not yet done — poll manually
result = wf.result(wait=True, poll_interval=5) # waits and polls every 5s
# Or check status without blocking:
if not wf.done():
print("Workflow still running. Call wf.result() again later.")
```
### Debugging tips
- **Check workflow status**: `wf.status`, check `wf.done()`, or call `wf.get_status()`
- **Inspect raw result**: `result.data` instead of convenience properties
- **Re-run failed workflow**: Save UUIDs and retry with `rowan.retrieve_workflow(uuid)`
- **Validate molecules beforehand**: Use RDKit or Chemaxon before batch submission
Common errors with their fixes, and debugging tips, are in
[references/troubleshooting.md](references/troubleshooting.md).
## Recommended usage patterns
@@ -0,0 +1,268 @@
---
title: "Batch Submission, Webhooks, and Asynchronous Workflows"
task: ""
lineage_type: import
upstream_source: https://github.com/K-Dense-AI/scientific-agent-skills/blob/72d742e1/skills/rowan/references/batch_and_webhooks.md
upstream_sha: 72d742e1
imported_at: 2026-08-29
prompt_class: prompt
upstream_changes: accepted
author: upstream
validated: false
---
# Batch Submission, Webhooks, and Asynchronous Workflows
Webhook secret management, batch submit/poll/retrieve, the non-blocking fire-and-check
pattern, webhook payloads and signature verification, and webhook best practices.
### Webhook secret management
For webhook signature verification, manage secrets through your user account:
```python
import rowan
# Get your current webhook secret (returns None if none exists)
secret = rowan.get_webhook_secret()
if secret is None:
secret = rowan.create_webhook_secret()
# These functions return the secret as a plain string.
# Rotate your secret (invalidates old, creates new)
# Use this periodically for security.
secret = rowan.rotate_webhook_secret()
# Verify incoming webhook signatures.
is_valid = rowan.verify_webhook_secret(
raw_body=b"...", # Raw request body (bytes)
signature_header="sha256=...", # Value from X-Rowan-Signature
secret=secret,
)
```
## Batch submission and retrieval
For libraries or analogue series, submit in a loop using the specific workflow function. The generic `rowan.batch_submit_workflow()` and `rowan.submit_workflow()` functions currently return 422 errors from the API — use the named functions (`submit_descriptors_workflow`, `submit_pka_workflow`, etc.) instead.
### Submit a batch
```python
smileses = ["CCO", "CC(=O)O", "c1ccccc1O"]
names = ["ethanol", "acetic acid", "phenol"]
workflows = [
rowan.submit_descriptors_workflow(rowan.Molecule.from_smiles(smi), name=name)
for smi, name in zip(smileses, names)
]
print(f"Submitted {len(workflows)} workflows")
```
### Poll batch status
```python
statuses = rowan.batch_poll_status([wf.uuid for wf in workflows])
# Returns aggregate counts — not per-UUID:
# {'queued': 0, 'running': 1, 'complete': 2, 'failed': 0, 'total': 3, ...}
if statuses["complete"] == statuses["total"]:
print("All workflows done")
elif statuses["failed"] > 0:
print(f"{statuses['failed']} workflows failed")
```
### Retrieve and collect results
```python
results = []
for wf in workflows:
try:
result = wf.result()
results.append(result.data)
except rowan.WorkflowError as e:
print(f"Workflow {wf.uuid} failed: {e}")
# Optionally aggregate into DataFrame
import pandas as pd
df = pd.DataFrame(results)
```
### Non-blocking / fire-and-check pattern
For long-running workflows where you don't want to hold a process open, submit workflows, save their UUIDs, and check back later in a separate process.
**Session 1 — submit and save UUIDs:**
```python
import rowan, json
rowan.api_key = "..."
smileses = ["CCO", "CC(=O)O", "c1ccccc1O"]
workflows = [
rowan.submit_descriptors_workflow(
rowan.Molecule.from_smiles(smi), name=f"compound_{i}"
)
for i, smi in enumerate(smileses)
]
# Save UUIDs to disk (or a database)
uuids = [wf.uuid for wf in workflows]
with open("workflow_uuids.json", "w") as f:
json.dump(uuids, f)
print("Submitted. Check back later.")
```
**Session 2 — check status and collect results when ready:**
```python
import rowan, json
rowan.api_key = "..."
with open("workflow_uuids.json") as f:
uuids = json.load(f)
results = []
for uuid in uuids:
wf = rowan.retrieve_workflow(uuid)
if wf.done():
result = wf.result(wait=False)
results.append({"uuid": uuid, "data": result.data})
else:
print(f"{uuid}: still running ({wf.get_status()})")
print(f"Collected {len(results)} completed results")
```
## Webhooks and asynchronous workflows
For long-running campaigns or when you don't want to keep a process alive, use webhooks to notify your backend when workflows complete.
### Setting up webhooks
Every workflow submission function accepts a `webhook_url` parameter:
```python
wf = rowan.submit_docking_workflow(
protein=protein,
pocket=pocket,
initial_molecule=rowan.Molecule.from_smiles("CCO"),
webhook_url="https://myserver.com/rowan_callback",
name="docking with webhook",
)
print(f"Workflow submitted. Result will be POSTed to webhook when complete.")
```
Webhook URLs can be passed to any specific workflow function (`submit_docking_workflow()`, `submit_pka_workflow()`, `submit_descriptors_workflow()`, etc.).
### Webhook authentication with secrets
Rowan supports webhook signature verification to ensure requests are authentic. You'll need to:
1. **Create or retrieve a webhook secret:**
```python
import rowan
# Create a new webhook secret
secret = rowan.create_webhook_secret() # returns a string
# Store it securely; do not log it.
# Or retrieve an existing secret
secret = rowan.get_webhook_secret()
# Rotate your secret (invalidates old one, creates new)
new_secret = rowan.rotate_webhook_secret()
```
2. **Verify incoming webhook requests:**
```python
import rowan
import hmac
import json
def verify_webhook(request_body: bytes, signature: str, secret: str) -> bool:
"""Verify the HMAC-SHA256 signature of a webhook request."""
return rowan.verify_webhook_secret(request_body, signature, secret)
```
### Webhook payload and signature
When a workflow completes, Rowan POSTs a JSON payload to your webhook URL with the header:
```text
X-Rowan-Signature: <HMAC-SHA256 signature>
```
The request body contains the complete workflow result:
```json
{
"workflow_uuid": "wf_12345abc",
"workflow_type": "docking",
"workflow_name": "lead docking",
"status": "COMPLETED_OK",
"created_at": "2025-04-01T12:00:00Z",
"completed_at": "2025-04-01T12:15:30Z",
"data": {
"scores": [-8.2, -8.0, -7.9],
"best_pose": {...},
"metadata": {...}
}
}
```
### Example webhook handler with signature verification (FastAPI)
```python
from fastapi import FastAPI, Request, HTTPException
import rowan
import json
app = FastAPI()
webhook_secret = rowan.get_webhook_secret() or rowan.create_webhook_secret()
@app.post("/rowan_callback")
async def handle_rowan_webhook(request: Request):
# Get request body and signature
body = await request.body()
signature = request.headers.get("X-Rowan-Signature")
if not signature:
raise HTTPException(status_code=400, detail="Missing X-Rowan-Signature header")
# Verify signature
if not rowan.verify_webhook_secret(body, signature, webhook_secret):
raise HTTPException(status_code=401, detail="Invalid webhook signature")
# Parse and process
payload = json.loads(body)
wf_uuid = payload["workflow_uuid"]
status = payload["status"]
if status == "COMPLETED_OK":
print(f"Workflow {wf_uuid} succeeded!")
result_data = payload["data"]
# Process result, update database, trigger next workflow, etc.
elif status == "FAILED":
print(f"Workflow {wf_uuid} failed!")
# Handle failure
# Respond quickly to prevent retries
return {"status": "received"}
```
### Webhook best practices
- **Always verify signatures** using `rowan.verify_webhook_secret()` to ensure requests are from Rowan
- **Respond quickly** (< 5 seconds); offload heavy processing to async tasks or background jobs
- **Implement idempotency**: workflows may retry; handle duplicate payloads gracefully using `workflow_uuid`
- **Log all events** for debugging and audit trails
- **Use for long campaigns**: webhooks shine with 50+ workflows; for small jobs, polling with `result()` is simpler
- **Rotate secrets regularly** using `rowan.rotate_webhook_secret()` for security
- **Return 2xx status** to confirm receipt; Rowan may retry on 5xx errors
@@ -0,0 +1,132 @@
---
title: "End-to-End Example: Lead Optimization Campaign"
task: ""
lineage_type: import
upstream_source: https://github.com/K-Dense-AI/scientific-agent-skills/blob/72d742e1/skills/rowan/references/end_to_end_example.md
upstream_sha: 72d742e1
imported_at: 2026-08-29
prompt_class: prompt
upstream_changes: accepted
author: upstream
validated: false
---
# End-to-End Example: Lead Optimization Campaign
A complete campaign: project and folder setup, tautomer selection, pKa and property
prediction across an analogue series, result collection and summary, and a docking
follow-up on the selected compound.
## End-to-end example: Lead optimization campaign
This example demonstrates a realistic workflow for optimizing a hit compound:
```python
import rowan
import pandas as pd
# 1. Create a project and folder for organization
project = rowan.create_project(name="CDK2 Hit Optimization")
rowan.set_project("CDK2 Hit Optimization")
folder = rowan.create_folder(name="round_1_tautomers_and_pka")
# 2. Load hit compound and analogues
hit = "CCNc1ncc(c(Nc2ccc(F)cc2)n1)-c1cccnc1" # Known hit
analogues = [
"CCNc1ncc(c(Nc2ccccc2)n1)-c1cccnc1", # Remove F
"CCNc1ncc(c(Nc2ccc(Cl)cc2)n1)-c1cccnc1", # Cl instead of F
"CCC(C)Nc1ncc(c(Nc2ccc(F)cc2)n1)-c1cccnc1", # Propyl instead of ethyl
]
# 3. Determine best tautomers (just in case)
print("Searching tautomeric forms...")
taut_workflows = [
rowan.submit_tautomer_search_workflow(
rowan.Molecule.from_smiles(smi), name=f"analog_{i}", folder=folder,
)
for i, smi in enumerate(analogues)
]
best_tautomers = []
for wf in taut_workflows:
result = wf.result()
best_tautomers.append(result.best_tautomer)
# 4. Predict pKa and basic properties for all analogues
print("Predicting pKa and properties...")
pka_workflows = [
rowan.submit_pka_workflow(
smi, method="chemprop_nevolianis2025", name=f"compound_{i}", folder=folder,
)
for i, smi in enumerate(best_tautomers)
]
descriptor_workflows = [
rowan.submit_descriptors_workflow(
rowan.Molecule.from_smiles(smi), name=f"compound_{i}", folder=folder
)
for i, smi in enumerate(best_tautomers)
]
# 5. Collect results
pka_results = []
for wf in pka_workflows:
try:
result = wf.result()
pka_results.append({
"compound": wf.name,
"pka": result.strongest_acid, # pKa of the strongest acid site
"uuid": wf.uuid,
})
except rowan.WorkflowError as e:
print(f"pKa prediction failed for {wf.name}: {e}")
descriptor_results = []
for wf in descriptor_workflows:
try:
result = wf.result()
desc = result.descriptors
descriptor_results.append({
"compound": wf.name,
"exact_mass": desc.get("MW"),
"topological_psa": desc.get("TopoPSA"),
"logp": desc.get("SLogP"),
"hba": desc.get("nHBAcc"),
"hbd": desc.get("nHBDon"),
"uuid": wf.uuid,
})
except rowan.WorkflowError as e:
print(f"Descriptor calculation failed for {wf.name}: {e}")
# 6. Merge and summarize
df_pka = pd.DataFrame(pka_results)
df_desc = pd.DataFrame(descriptor_results)
df = df_pka.merge(df_desc, on="compound", how="outer")
print("\n=== Preliminary SAR ===")
print(df.to_string())
# 7. Select promising compound for docking
# compound names are "compound_0", "compound_1", etc. — extract the index
top_idx = int(df.loc[df["pka"].idxmin(), "compound"].split("_")[1])
top_smiles = best_tautomers[top_idx]
print(f"\nProceeding with docking: {top_smiles}")
# 8. Docking campaign
protein = rowan.create_protein_from_pdb_id(code="1CKP", name="CDK2_1CKP")
pocket = [[10.5, 24.2, 31.8], [18.0, 18.0, 18.0]]
docking_wf = rowan.submit_docking_workflow(
protein=protein,
pocket=pocket,
initial_molecule=rowan.Molecule.from_smiles(top_smiles),
do_pose_refinement=True,
name=f"docking_{top_idx}",
)
dock_result = docking_wf.result()
print(f"\nDocking score: {dock_result.scores[0]:.2f} kcal/mol")
print(f"Best pose saved to: best_pose.pdb")
dock_result.best_pose.write("best_pose.pdb")
```
@@ -0,0 +1,119 @@
---
title: "Error Handling and Troubleshooting"
task: ""
lineage_type: import
upstream_source: https://github.com/K-Dense-AI/scientific-agent-skills/blob/72d742e1/skills/rowan/references/troubleshooting.md
upstream_sha: 72d742e1
imported_at: 2026-08-29
prompt_class: prompt
upstream_changes: accepted
author: upstream
validated: false
---
# Error Handling and Troubleshooting
Common errors — invalid SMILES, missing API keys, HTTP/API failures, failed
workflows, and polling — with verified handling for `rowan-python` 3.1.13.
## Actual exception classes
`rowan.ValidationError`, `rowan.AuthenticationError`, and
`rowan.InsufficientCreditsError` do **not** exist in SDK 3.1.13. Referencing one
in an `except` clause raises `AttributeError` while handling the original
failure.
| Failure | Exception |
|---|---|
| Bad SMILES or wrong input type for a workflow | `ValueError` |
| Authentication, credit, or other HTTP/API failure | `httpx.HTTPStatusError` |
| Submitted workflow fails server-side | `rowan.WorkflowError` |
## Validate molecules before submission
```python
from rdkit import Chem
smiles = "CCCC(CC"
mol = Chem.MolFromSmiles(smiles)
if mol is None:
raise ValueError(f"Invalid SMILES: {smiles}")
```
Input types vary by workflow. For example, descriptors require a molecule
object, while pKa accepts a SMILES string:
```python
import rowan
try:
rowan.submit_descriptors_workflow("CCO")
except ValueError as exc:
print(f"Input problem: {exc}")
wf = rowan.submit_descriptors_workflow(rowan.Molecule.from_smiles("CCO"))
```
## Authentication and API errors
```python
import httpx
import rowan
try:
user = rowan.whoami()
except httpx.HTTPStatusError as exc:
if exc.response.status_code == 401:
print("Bad or missing API key — check ROWAN_API_KEY")
else:
# Includes credit limits and other API failures; inspect the response.
print(exc.response.status_code, exc.response.text)
raise
```
The SDK treats an environment variable set to an **empty string** as present.
That produces `401 Could not validate credentials` rather than a clear missing
key error. Check that `ROWAN_API_KEY` is non-empty without printing the key:
```python
import os
api_key = os.environ.get("ROWAN_API_KEY")
if not api_key:
raise RuntimeError("ROWAN_API_KEY is missing or empty")
```
Use `max_credits=N` on submission calls to bound spend.
## Server-side workflow failures
```python
try:
result = wf.result()
except rowan.WorkflowError as exc:
print(f"Workflow failed: {exc}")
print(f"Status: {wf.get_status()}")
```
## Polling and non-blocking checks
```python
# Block and poll every five seconds.
result = wf.result(wait=True, poll_interval=5)
# Or check without blocking.
if not wf.done():
print(f"Still running: {wf.get_status()}")
else:
result = wf.result(wait=False)
```
`WorkflowResult.complete` is a boolean, not a percent-done value. For coarse
status, use `wf.get_status()` and `wf.fetch_latest()`.
## Debugging tips
- Inspect `result.data` when a convenience property is unavailable.
- Save workflow UUIDs and reconnect with `rowan.retrieve_workflow(uuid)`.
- Use `dir(result)` to discover properties for that result class; they differ.
- Validate SMILES locally with RDKit before any paid submission.
@@ -2,9 +2,9 @@
title: "Stable Baselines3 Callback System"
task: ""
lineage_type: import
upstream_source: https://github.com/K-Dense-AI/scientific-agent-skills/blob/9c9bd2e9/skills/stable-baselines3/references/callbacks.md
upstream_sha: 9c9bd2e9
imported_at: 2026-06-27
upstream_source: https://github.com/K-Dense-AI/scientific-agent-skills/blob/72d742e1/skills/stable-baselines3/references/callbacks.md
upstream_sha: 72d742e1
imported_at: 2026-08-29
prompt_class: prompt
upstream_changes: accepted
author: upstream
@@ -580,5 +580,5 @@ class DebugCallback(BaseCallback):
## Additional Resources
- Official SB3 Callbacks Guide: https://stable-baselines3.readthedocs.io/en/master/guide/callbacks.html
- Callback API Reference: https://stable-baselines3.readthedocs.io/en/master/common/callbacks.html
- Callback API Reference: https://stable-baselines3.readthedocs.io/en/master/guide/callbacks.html#module-stable_baselines3.common.callbacks
- TensorBoard Documentation: https://www.tensorflow.org/tensorboard
@@ -2,9 +2,9 @@
title: "Vectorized Environments in Stable Baselines3"
task: ""
lineage_type: import
upstream_source: https://github.com/K-Dense-AI/scientific-agent-skills/blob/9c9bd2e9/skills/stable-baselines3/references/vectorized_envs.md
upstream_sha: 9c9bd2e9
imported_at: 2026-06-27
upstream_source: https://github.com/K-Dense-AI/scientific-agent-skills/blob/72d742e1/skills/stable-baselines3/references/vectorized_envs.md
upstream_sha: 72d742e1
imported_at: 2026-08-29
prompt_class: prompt
upstream_changes: accepted
author: upstream
@@ -589,5 +589,5 @@ model = PPO.load("model", env=eval_env)
## Additional Resources
- Official SB3 VecEnv Guide: https://stable-baselines3.readthedocs.io/en/master/guide/vec_envs.html
- VecEnv API Reference: https://stable-baselines3.readthedocs.io/en/master/common/vec_env.html
- VecEnv API Reference: https://stable-baselines3.readthedocs.io/en/master/guide/vec_envs.html#module-stable_baselines3.common.vec_env
- Multiprocessing Best Practices: https://docs.python.org/3/library/multiprocessing.html
@@ -2,9 +2,9 @@
title: "Adverse Event Detection - Tool Parameter Reference"
task: ""
lineage_type: import
upstream_source: https://github.com/mims-harvard/ToolUniverse/blob/e2520a96/skills/tooluniverse-adverse-event-detection/TOOL_REFERENCE.md
upstream_sha: e2520a96
imported_at: 2026-06-26
upstream_source: https://github.com/mims-harvard/ToolUniverse/blob/4d14233e/skills/tooluniverse-adverse-event-detection/TOOL_REFERENCE.md
upstream_sha: 4d14233e
imported_at: 2026-08-18
prompt_class: unknown
upstream_changes: accepted
author: upstream
@@ -29,7 +29,7 @@ Verified parameter names, response formats, and fallback chains for all tools us
| `FAERS_count_reportercountry_by_drug_event` | `medicinalproduct` (REQUIRED), `patientsex`, `patientagegroup`, `serious` | Returns [{term: "US"/"GB"/..., count}] |
| `FAERS_search_adverse_event_reports` | `medicinalproduct`, `limit` (max 100), `skip` | Returns individual case reports with patient/drug/reaction data |
| `FAERS_search_reports_by_drug_and_reaction` | `medicinalproduct` (REQUIRED), `reactionmeddrapt` (REQUIRED), `limit`, `skip`, `patientsex`, `serious` | Returns individual reports filtered by specific reaction |
| `FAERS_search_serious_reports_by_drug` | `medicinalproduct` (REQUIRED), `seriousnessdeath`, `seriousnesshospitalization`, `seriousnesslifethreatening`, `seriousnessdisabling`, `limit` | Returns serious event reports |
| `FAERS_search_serious_reports_by_drug` | `medicinalproduct` (REQUIRED), `serious`, `seriousnessdeath`, `seriousnesshospitalization`, `seriousnesslifethreatening`, `seriousnessdisabling`, `limit` | Case reports. Despite the name it returns serious AND non-serious reports unless you pass `serious='Yes'` or one of the `seriousness*` criteria |
## FAERS Analytics Tools (operation-based)
@@ -1,8 +1,8 @@
---
lineage_type: import
upstream_source: https://github.com/mims-harvard/ToolUniverse/blob/cfd26718/skills/tooluniverse-biomedical-fact-lookup/SKILL.md
upstream_sha: cfd26718
imported_at: 2026-08-08
upstream_source: https://github.com/mims-harvard/ToolUniverse/blob/4d14233e/skills/tooluniverse-biomedical-fact-lookup/SKILL.md
upstream_sha: 4d14233e
imported_at: 2026-08-18
prompt_class: unknown
upstream_changes: accepted
name: tooluniverse-biomedical-fact-lookup
@@ -14,6 +14,36 @@ when_to_use: "A factual biomedical question has a single database-checkable answ
Factual biomedical questions — "which gene is in set X", "which gene is associated with disease Y according to DisGeNet", "which gene has a TF binding site per GTRD" — have an authoritative answer in a public database. Guessing from memory is unreliable (≈chance on niche annotations); the matching ToolUniverse tool returns the ground truth.
## Six traps that produce a confidently wrong answer
Each was observed producing a wrong answer on a real question. Check them before
answering; the detail for each is further down.
1. **"Highest p-value" in GWAS means most significant** — the *smallest* number.
Read literally it picks the study's weakest hit (`rs2476491` at 1e-06 instead
of `rs7775055-G` at 3e-174).
2. **A window of "N bp upstream plus M bp downstream" spans N+M+1 bases** — the
anchor counts. 100 either side of a TSS is 201 nt, not 200. Check the length
you got against the length you asked for.
3. **HPA subcellular locations pool every cell line the antibody was tested in.**
Report both `main_locations` and `additional_locations`, but when the question
names a line, treat them as candidates and drop annotations belonging to
another line — reciting all five is as wrong as reciting one.
4. **Allen Brain: answer the leaf structure, not its parent.** The atlas colours
the specific structure and gives the parent a different colour, so
"Hypothalamus" is wrong where `Lateral preoptic area` (#F2483B) is right.
`AllenBrain_search_structures` returns `color_hex_triplet`.
5. **SCREEN's `is_proximal` is unreliable; filter on `element_type`**`PLS`
and `pELS` are TSS-proximal, `dELS` distal.
6. **Derived scores are release-pinned.** gnomAD pLI for APOC2 is 0.047 in r4
and 0.402 in r2.1 — an 8.5x difference for the same gene. Set the release the
question names and say which you used.
## RULE ZERO: Look it up, never guess
If a question names a database, a gene set, or any annotation that lives in a database, you MUST query the tool before answering. Answering a "according to <database>" question from memory is a failure mode — these annotations (predicted miRNA targets, ChIP-seq binding, curated gene sets, disease associations) are exactly what models hallucinate. A tool-verified answer beats any recalled fact.
@@ -43,9 +73,59 @@ Most of these questions are MCQ with an "Insufficient information to answer the
| **drug / compound** target, MoA, approval | `ChEMBL_*`, `OpenFDA_*`, `GtoPdb_*`, `PubChem_*` | resolve drug, query the relation |
| **which drug for this patient** (clinical vignette naming a modifier) | `FDA_*_by_drug_name` — pick the section by modifier | see "Drug choice for a described patient" below |
| **protein** function / domain / sequence | `UniProt_*` | resolve accession, read annotation |
| **protein localization / expression** "according to the Human Protein Atlas" | `HPA_get_subcellular_location`, `HPA_get_rna_expression_by_source`, `HPA_get_comprehensive_gene_details_by_ensembl_id` | pass the gene symbol — an **antibody ID such as `HPA073143` also works** and resolves to its target gene. **Report main *and* additional locations** — see below |
| **brain region** in the Allen Mouse/Human Brain Atlas | `AllenBrain_search_structures` (`name` or `acronym`), `AllenBrain_get_structure` | reference-atlas regions are **colour-coded**: the result carries `color_hex_triplet`, so "the region shown in red" is answerable — see below |
| **regulatory element / cCRE** near a gene (ENCODE SCREEN) | `SCREEN_search_cCREs_by_region` | filter on `element_type` (**PLS** and **pELS** are TSS-proximal, **dELS** distal) and read `dnase_zscore` |
| **which variant is at / overlaps** a genomic region (ClinVar) | `ClinVar_search_by_region` | **not** `ClinVar_search_variants` — Entrez matches a variant's START, so a narrow window misses a CNV that spans the region but begins megabases upstream. Returns true overlaps, smallest span first |
| **how many peaks / which datasets** for a TF experiment (ReMap) | `ReMap_list_datasets_for_target` | one GEO series can hold several datasets (GSE23852/FOXA1 = 2, with 60,158 and 67,736 peaks) — report them separately unless a total is asked for; `count_peaks: true` to get counts |
| **protein interaction partners** (STRING) | `STRING_get_protein_interactions` | read the **`partner`** field, not `preferredName_B`: edges are ordered A/B by internal ID, so the queried protein sits in column A on about half of them |
When unsure which tool wraps a database, search the catalog by the *relation* (e.g. "gene disease association", "gene set members"), not the brand name — ToolUniverse usually already has it.
### Allen Brain Atlas — answer with the specific structure, not its parent
The reference atlas colours every structure, and `AllenBrain_search_structures`
returns `color_hex_triplet`. A question naming a colour ("which region is
annotated in red at coronal position 181") is asking which **leaf structure**
carries that colour, e.g. `Lateral preoptic area` = `#F2483B`.
Answering with the enclosing region ("Hypothalamus") is wrong even though it
contains the right area: the atlas colours the specific structure, and the
parent has its own different colour. Search by name or acronym, compare
`color_hex_triplet`, and give the structure whose colour matches. Note the same
acronym can return several rows (hemisphere-specific and ontology-version
entries) with different colours — prefer the row whose `name` matches the
question's wording.
### Human Protein Atlas — report both location fields
`HPA_get_subcellular_location` splits its answer in two, and the split is not
significance ranking:
```
main_locations : ['Nucleoplasm']
additional_locations : ['Primary cilium', ..., 'Cytosol']
```
A question asking "what localization does this antibody show" wants the
locations HPA reports, which is **both lists** — answering from `main_locations`
alone drops real localizations and is a common way to be half-right (e.g.
answering "Nucleoplasm" where HPA reports "Nucleoplasm, Cytosol"). Use
`location_summary`, which already joins them, or read both fields.
Two further cautions:
- **Locations aggregate over cell lines.** HPA pools immunofluorescence across
every line an antibody was tested in. If the question names one line (HEK293,
U-2 OS), treat the list as the candidate set and say which line you are
reporting for, rather than implying the aggregate is line-specific.
- **Per-cell-type RNA values are only published for enriched cell types.** HPA's
machine-readable fields give specificity plus nTPM/nCPM for the cell types a
gene is enriched in; a value for an arbitrary cell type is not exposed. If a
question asks for one that is absent, say so instead of substituting the
nearest available number — those differ by an order of magnitude.
## MSigDB set-name conventions (the most common LAB-Bench pattern)
ToolUniverse's `MSigDB_*` tools cover several collections that LAB-Bench questions are built from. Get the set name right:
@@ -127,6 +207,42 @@ Try the `MP_<PHENOTYPE>` MSigDB set first (above): it answers in one call per op
## Computational procedures (when the answer is COMPUTED, not looked up)
### GWAS "highest p-value" means most significant
In GWAS writing, "the highest p-value", "the top hit" and "the strongest
association" all mean the **most significant** result — the *smallest* numeric
p-value. Read literally, "highest" picks the weakest association in the study
and is almost never what was meant.
For GCST005528 the literal reading gives `rs2476491-?` at p = 1e-06; the
intended answer is `rs7775055-G` at p = 3e-174.
Sort ascending by p-value and report that hit. If the phrasing genuinely could
go either way, give the most significant one and say in a clause that the
numerically largest p-value is a different SNP — do not silently pick the
literal reading.
### Genomic windows — count the anchor base
A window described as "N bp upstream plus M bp downstream of X" spans
**N + M + 1** bases, because the anchor base X is itself included. Asking for
100 up and 100 down around a TSS is 201 nt, not 200. Off-by-one here is the
single most common way a sequence answer is wrong while looking right.
The same care applies to the coordinate convention of whichever tool you call:
| convention | span of `start`..`end` | used by |
|---|---|---|
| 1-based inclusive | `end - start + 1` | Ensembl `region`, UCSC browser text, IGV, samtools |
| 0-based half-open | `end - start` | UCSC REST API, BED |
`UCSC_get_sequence` takes a written locus via `region` (1-based inclusive) or
explicit `chrom`/`start`/`end` with `coordinate_system`; it echoes
`region_1based` and `requested_length` so the span is checkable. **Always check
the returned length against what the question asked for** before answering — a
sequence of the wrong length is wrong even when every base you kept is right.
Any question with a **single deterministic numeric/combinatorial answer** must be obtained by **RUNNING code**, never by estimating or doing it in your head. This covers sequence questions (ORF counts, restriction fragments/sizes, GC content, translation) **and** any other exactly-computable question — e.g. **genetics segregation / Mendelian or polyploid gamete ratios, combinatorial probabilities, stoichiometry, dosage/PK arithmetic, counting problems**. Mental arithmetic on these is the #1 avoidable error: the model reliably mis-counts or mis-multiplies. If a question reduces to "enumerate the cases / multiply the probabilities / count the objects", **write a short Python snippet, execute it, and report exactly what it returns** — even when the topic looks like a biology "reasoning" question, if the answer is a definite number, compute it rather than reason it out. Match the question's wording for conventions (which strand; linear vs circular; which cross/segregation model) and **state the convention you used** so the answer is auditable.
**Final-answer discipline (avoid "computed right, answered wrong").** After the code returns the value, map it back to the option letters **carefully and explicitly**: quote the computed value, then find the option that matches it exactly (for a set of fragment sizes, match the whole multiset; for a count, match the integer). A surprising number of misses are cases where the computation was correct but the wrong letter was selected — do not let this happen; re-read each option against the computed result before emitting `[ANSWER]`.
@@ -219,5 +335,19 @@ Interpretation: report the **exact value the code returns** (ORF count; fragment
## Limitations (honest)
- **Key-gated sources**: `DisGeNET_*` and OMIM tools need `DISGENET_API_KEY` / OMIM key. Without a key, fall back to `OpenTargets_*` / `MyDisease_*` (keyless) and state the source used. If no keyless source can answer and the question is database-specific, this is a genuine "Insufficient information" case — say so.
- **Release mismatch**: a tool's snapshot of a database may differ slightly from the exact release a question cites; report the source and version when it matters.
- **Release mismatch**: a tool's snapshot of a database may differ from the exact
release a question cites — and for some quantities the difference is not
slight. Derived scores get recomputed between releases, so the *same gene* can
differ by an order of magnitude. gnomAD pLI, via `gnomad_get_constraint`:
| gene | gnomAD r4 | gnomAD r2.1 |
|---|---|---|
| APOC2 | 0.046875 | 0.401638 |
| APOC1 | 0.086323 | 0.216848 |
Where a tool exposes a `dataset`/release parameter, set it to the release the
question names and **say which release you used**. If the question names one
the tool cannot serve, report the release you did use rather than presenting
the number as if it were release-independent — a bare pLI value is ambiguous
by a factor of eight here.
- This skill grounds *factual* lookups. For computing over user data files, use the data-analysis router skills instead.
@@ -1,8 +1,8 @@
---
lineage_type: import
upstream_source: https://github.com/mims-harvard/ToolUniverse/blob/cfd26718/skills/tooluniverse-phylogenetics/SKILL.md
upstream_sha: cfd26718
imported_at: 2026-08-08
upstream_source: https://github.com/mims-harvard/ToolUniverse/blob/4d14233e/skills/tooluniverse-phylogenetics/SKILL.md
upstream_sha: 4d14233e
imported_at: 2026-08-18
prompt_class: catalogue
upstream_changes: accepted
name: tooluniverse-phylogenetics
@@ -12,6 +12,45 @@ disable-model-invocation: true
# Phylogenetics and Sequence Analysis
## Four traps that produce a confidently wrong number
Each of these was observed producing a wrong answer *while the correct guidance
was already present further down this file*. Check them before you answer.
1. **PhyKIT prints more than one column, and for `saturation` the two
conventions disagree — state which you used.** `phykit saturation` prints
`saturation <TAB> |saturation-1|`. Its own `--help` is explicit: *"The first
value is the saturation value and the second column is the absolute value of
saturation minus 1."* But several published analyses (and some reference
answers derived from them) report the **second** column as "the saturation
value". The two always sum to 1.0000, which is the tell that you may be
looking at the wrong one — on the fungal scogs set the medians are 0.39
(col 1) and 0.61 (col 2).
So: **follow phykit and use column 1** unless the question or source defines
saturation the other way, and say in your answer which column you read. Do
not silently pick the one that looks closer to an expected number.
`treeness_over_rcv` has no such ambiguity: it gives
`ratio <TAB> treeness <TAB> RCV` and the ratio is first.
2. **"Gap percentage" means the fraction of alignment COLUMNS containing at
least one gap**, not the fraction of residues that are gaps. On the fungal
scogs set the residue definition maxes out at 0.556, so a ">70% gaps" filter
selects **nothing** and the question looks unanswerable; by columns, three
orthologs qualify (max 0.783).
3. **`treeness_over_rcv` and `rcv` take the UNTRIMMED `.faa.mafft`**, while
`saturation` takes the trimmed `.clipkit`. RCV measures variability across
columns, so trimming changes it: median 0.2683 untrimmed against 0.3050
trimmed, and among >70%-gap genes the maximum is 0.2572 untrimmed against
0.4174 trimmed.
4. **Never loop PhyKIT per file.** `phykit_batch_analysis` is parallel and does
~250 trees in about 35 seconds; a shell loop takes ~9 minutes and runs out of
turns mid-way, producing no answer at all. It also selects the right column
for every function, which removes trap 1 entirely.
## RULE ZERO — Check for pre-computed results FIRST
Before following any instruction below, scan the data folder for:
@@ -182,8 +221,9 @@ set (249 orthologs, canonical shipped files):
```
median treeness/RCV untrimmed .faa.mafft = 0.2683 trimmed .clipkit = 0.3050
max treeness/RCV (>70% gap genes)
untrimmed .faa.mafft = 0.1866 trimmed .clipkit = 0.4205
max treeness/RCV (over the 3 genes with >70% gapped columns:
1260807at2759 0.0861, 1567796at2759 0.1866, 939345at2759 0.2572)
untrimmed .faa.mafft = 0.2572 trimmed .clipkit = 0.4174
```
Plain `treeness` needs no alignment and is unaffected — it reproduces exactly
@@ -351,6 +391,63 @@ tu run phykit_batch_analysis '{"operation":"gap_percentage","directory":"./align
```
Do NOT run phykit manually in a loop — the tool handles all files and returns correct summary statistics.
**The batch tool is parallel: ~250 trees finish in about 35 seconds.** A per-tree
shell loop takes ~9 minutes for the same work and is the single most common way
these questions end with no answer at all — the run hits its turn or time budget
mid-loop and reports "I'll report when it finishes" instead of a number. If you
find yourself writing `for f in *.treefile`, stop and call the batch tool.
Supported `function` values include `treeness`, `saturation`, `dvmc`,
`long_branch_score`, `total_tree_length`, `parsimony_informative`,
`treeness_over_rcv` (alias `toverr`). `dvmc` and `long_branch_score` are
covered — you do not need to loop for those.
**Two-group comparisons (Mann-Whitney U, differences of medians).** Questions
comparing fungi against animals need one batch call per group, then the test on
the two value lists — not a per-tree loop over both groups:
```bash
tu run phykit_batch_analysis '{"operation":"batch","function":"dvmc","directory":"<fungi>","extension":".treefile"}'
tu run phykit_batch_analysis '{"operation":"batch","function":"dvmc","directory":"<animals>","extension":".treefile"}'
# then scipy.stats.mannwhitneyu(fungi_values, animal_values)
```
Ask for `values` in the result when you need the full list for a test; the batch
tool returns them for sets up to 50 and summary statistics always. For larger
sets, compute the statistic from the per-group summaries the tool returns rather
than re-deriving every value by hand.
### PhyKIT column conventions — take the right one
Several PhyKIT subcommands print more than one number per file, and the value
the question wants is usually not the first:
| subcommand | prints | the value asked for |
|---|---|---|
| `saturation` | `saturation <TAB> \|saturation-1\|` | **column 1** per phykit's docs; some sources report col 2 — say which you used |
| `treeness_over_rcv` | `treeness/RCV <TAB> treeness <TAB> RCV` | **column 1**, the ratio |
| `parsimony_informative_sites` | `n_pi <TAB> n_total <TAB> %PIS` | column 3 for a percentage |
Taking `saturation`'s first column gives exactly `1 - answer`: a fungal set
whose saturation is 0.6146 reports 0.3854 instead, and the two sum to 1.0000,
which is the tell. `phykit_batch_analysis` already selects the right column for
each function — another reason to call it rather than run the CLI yourself.
### Commit the value you computed
Two failures in this benchmark came from computing the right number and then
answering a different one:
- a tree-length ratio computed as **2.1775**, then answered as 1.9 after
re-reading "paired orthologs";
- an average treeness that listed **19** among the alternatives, then committed 10.
When a question is ambiguous, compute the reading you judge most literal, state
the alternative in one clause, and **answer with the value you actually
computed**. Do not replace a computed result with a re-derived one at the last
step — if two readings are both defensible, give the computed number first and
name the other, rather than silently switching.
### PhyKIT column-position cheat sheet (parse output carefully)
When parsing PhyKIT stdout for batch metrics, the **column you want** depends on the metric: