AnkusDrive
Drives FreeCAD through its Python API, providing CLI and MCP tools to create documents, build sketches, extrude solids, mesh parts, run FEM simulations (CalculiX/Elmer), read back stress/displacement fields, and manage design-control workflows.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@AnkusDrive design a 40x30x10mm mounting bracket and run a static FEM check"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
AnkusDrive
A CLI + MCP server that drives FreeCAD through its Python API so LLMs (and humans at a terminal) can design mechanical parts and run FEM simulations without clicking through the GUI.
Why
FreeCAD exposes almost everything it does through a Python API — create documents, build sketches, extrude solids, mesh them, run CalculiX/Elmer FEM solves, read back stress/displacement fields. But that API lives inside FreeCAD's embedded Python (freecadcmd), which is awkward to call from anywhere else. AnkusDrive wraps it behind two surfaces:
CLI — one-shot commands (
ankusdrive run script.py,ankusdrive box --w 10 --d 20 --h 5 -o part.FCStd) for scripts, CI, and quick iteration.MCP server — 280+ structured tools (
new_document,add_primitive,boolean_op,pad,add_gear,fem_new_analysis,fem_run,fem_results) so an LLM agent can model, inspect, and simulate iteratively. Beyond core CAD/FEM this now spans a broad simulation surface (thermal, CFD/CHT, EM, acoustics, FSI, injection molding, granular/DEM, optics, multibody) and a design-control layer (item/part numbers, recipes, variant families, lifecycle/revision, ECO change orders, versioned interfaces).Multi-agent orchestration — a host-side reference layer that lets a team of agents partition one product into components, build them in parallel, and merge the pieces back together with the joints actually fitting (see Multi-agent design).
Related MCP server: FreeCAD MCP Server
Target environment
FreeCAD 1.1.x. The
freecadcmdbinary is auto-discovered per-OS (macOS.appbundle, Linux/usr/binetc., WindowsC:\Program Files\FreeCAD 1.1\bin\freecadcmd.exe— version-globbed); override via$ANKUSDRIVE_FREECADCMDor rely on PATH. Runankusdrive doctorto see exactly what resolved.Bundled Python,
ccx(CalculiX), andgmshalready ship inside every FreeCAD install — the macOS.app, the Linux package, and the Windowsbin\— so core CAD + structural FEM work on all three with no extra install.Host-side rendering needs
Pillowandnumpy; both are installed by AnkusDrive as regular pip deps.One optional exception: drawing PDF/SVG export (
export_drawing) renders inside FreeCAD's bundled Python, so it needsreportlab+svglibinstalled there — see Drawing export (PDF/SVG). DXF export and everything else leave FreeCAD's Python untouched.
Setup
AnkusDrive is a pip-installable package; FreeCAD itself is the only thing you
install separately. The host-side dependencies (mcp, Pillow, numpy) come
along with the install. freecadcmd is launched as a subprocess and uses its
own bundled Python — AnkusDrive doesn't touch it.
# 1. Install FreeCAD 1.1.x from https://www.freecad.org/
# (macOS: drag to /Applications; Linux: distro package or AppImage;
# Windows: run the installer — default C:\Program Files\FreeCAD 1.1)
# 2. Install AnkusDrive. Pick one:
pipx install ankusdrive # from PyPI — isolated app, `ankusdrive` on PATH
pip install ankusdrive # or into an env you manage yourself
# unreleased main, or for development from a clone:
pipx install git+https://github.com/gchen19/AnkusDrive.git
git clone https://github.com/gchen19/AnkusDrive.git && cd AnkusDrive
python3 -m venv .venv && .venv/bin/pip install -e . # `.venv/bin/ankusdrive`
# 3. Smoke-test that the worker can reach FreeCAD, and see the full setup report
ankusdrive ping # → ping=pong freecad=1.1.1
ankusdrive doctor # per-item FreeCAD + solver checklist with the exact fix eachOn Windows, don't follow the block above by hand — there is one scripted path that does all of it including the MCP registration: Windows quickstart (PowerShell).
AnkusDrive is published on PyPI at
pypi.org/project/ankusdrive; the
distribution roadmap beyond it (marketplace listings, hosted transport) is
tracked in epic #303; the
original phase plan is kept as a design record at
docs/archive/PUBLISHING_PLAN.md.
macOS quickstart — solvers in a container
FreeCAD, CalculiX, SU2, PrusaSlicer and every pip-wheel family run natively on a Mac; the block above is all you need for those. What has no practical macOS build is the Linux solver stack — OpenFOAM, Elmer, YADE, openEMS, Bempp, preCICE, openInjMoldSim. Those run in a container, and AnkusDrive stays on the host and reaches into it. The image is multi-arch, so on Apple Silicon it runs native, not emulated.
# 1. A container engine: Docker Desktop, OrbStack, colima or podman.
# 2. Pull the solver image (0.88 GB on Apple Silicon, 1.15 GB on Intel).
docker pull ghcr.io/gchen19/ankusdrive-solvers:latest
# 3. Check it is ours before running your geometry through it (see below).
bash scripts/verify-container-image.sh
# 4. Point AnkusDrive at the container substrate.
export ANKUSDRIVE_SUBSTRATE=container
# optional: ANKUSDRIVE_CONTAINER_ENGINE=podman|nerdctl (default docker)
# optional: ANKUSDRIVE_CONTAINER=<name> (default ankusdrive-solvers)
# 5. Create the container. $TMPDIR must be mounted at the SAME path inside, because a
# case directory has to mean the same thing on both sides. On macOS $TMPDIR is a
# per-user /var/folders/... path — mount THAT, not /tmp.
docker run -d --name ankusdrive-solvers \
--user "$(id -u):$(id -g)" -e HOME=/tmp \
--network none --cap-drop ALL --security-opt no-new-privileges \
--read-only --tmpfs /tmp:rw,exec,size=2g \
-v "$TMPDIR:$TMPDIR" \
ghcr.io/gchen19/ankusdrive-solvers sleep infinity
# 6. Export the in-container paths for the OpenFOAM-backed families. The image
# publishes them; YADE, Elmer, openEMS and Bempp need no export — AnkusDrive finds
# them by asking the container.
docker exec ankusdrive-solvers env | grep -E \
'^ANKUSDRIVE_(OPENFOAM_PATH|OPENFOAM_BASHRC|FSI_OPENFOAM_BASHRC|CCX_PRECICE|PRECICE_LIB|OPENFOAM_ADAPTER_LIB|OPENINJMOLDSIM|OPENINJMOLDSIM_BASHRC)='
# 7. Confirm.
ankusdrive doctor # each family: ready via <solver> (in container)
ankusdrive doctor --verify-image # …and that the image is signed by this repoThe run flags are least privilege, and each is there because the solvers genuinely
do not need what it removes — verified by running the live solver suites with them on.
--network none in particular: nothing in a mesh is a reason to reach the internet.
Don't copy the image's ANKUSDRIVE_FREECADCMD or ANKUSDRIVE_CALCULIX_PATH — those
name paths inside the container, while FreeCAD and ccx run on your Mac.
A config.toml written for a native install is the one trap here: its absolute paths
are read as in-container paths. ankusdrive doctor now catches that and says so.
The alternative substrate on macOS is a Multipass VM, which you provision yourself —
docs/MACOS.md has the full per-solver reality on a Mac, and
docs/CONTAINER_SUBSTRATE.md the container path in depth.
The solver images, and checking they are ours
image | what it is | size |
| what you want: solvers and their runtime libraries, nothing else | 1.15 GB amd64 / 0.88 GB arm64 |
| the CI image — also carries FreeCAD, the driver venv and every build toolchain, because the whole test suite runs inside it | 5.3 GB / 4.5 GB |
Both are public, multi-arch (linux/amd64 + linux/arm64, each built natively) and
tagged latest plus sha-<commit>.
Every published manifest is signed through Sigstore with a short-lived GitHub OIDC identity — no key to store or leak — and carries provenance naming the repository, workflow and commit that built it, plus a CycloneDX SBOM of what is inside:
bash scripts/verify-container-image.sh # the slim image, :latest
bash scripts/verify-container-image.sh ghcr.io/gchen19/ankusdrive-solvers@sha256:<digest>Needs the GitHub CLI (gh ≥ 2.49, authenticated). Verify a digest and then run
that digest: verifying :latest today and pulling :latest next week are two
different images. ankusdrive doctor prints the digest the running container was made
from, and --verify-image checks it.
There are three answers, and only one is alarming: verified; unsigned (an image
you built yourself, or one published before signing existed — silence it with
ANKUSDRIVE_ALLOW_UNVERIFIED_IMAGE=1); and mismatch, an image carrying provenance
from somewhere else, which no setting silences. Verification never blocks a solve.
Building your own — a subset, or with your own changes — takes minutes, because nothing is compiled (the prebuilt solver trees are copied):
tools/build_solver_image.sh --solvers "openfoam fsi" -t my-solvers:dev # 0.69 GBWindows quickstart (PowerShell)
Windows is a first-class target (core CAD + CalculiX FEM run natively against a stock
FreeCAD 1.1 install), and the whole core install is one script — venv, pinned
dependencies, doctor, and the MCP registration line with resolved absolute paths:
# 1. Install FreeCAD 1.1.x from https://www.freecad.org/ (default C:\Program Files\FreeCAD 1.1).
# Nothing needs to go on PATH — AnkusDrive globs the versioned install dir itself.
# 2. Clone and run the core installer. Windows PowerShell 5.1 is enough; no admin needed.
git clone https://github.com/gchen19/AnkusDrive.git
cd AnkusDrive
powershell -ExecutionPolicy Bypass -File scripts\install-core.ps1That creates .venv, installs AnkusDrive with the pins that matter (notably mcp<2 —
mcp 2.x installs cleanly and then breaks ankusdrive mcp), verifies the resolved
mcp/numpy/Pillow, runs ankusdrive doctor + ankusdrive ping, completes a real MCP
stdio handshake, and finally prints your registration block. Useful switches:
-Python 'C:\Program Files\Python313\python.exe' to pick an interpreter,
-Extras mbd,fluids for the pip-wheel solver families, -Persist to write the FreeCAD
path into %APPDATA%\ankusdrive\config.toml (MCP hosts launch with a minimal
environment, so a $env: set in your terminal will not reach them).
3. Register it with your MCP host. The script prints these with your real paths
filled in — a GUI host doesn't inherit your shell PATH, so the absolute path matters:
# Claude Code
claude mcp add ankusdrive -- C:\Users\you\AnkusDrive\.venv\Scripts\ankusdrive.exe mcp
# Claude Desktop: %APPDATA%\Claude\claude_desktop_config.json
# { "mcpServers": { "ankusdrive": {
# "command": "C:\\Users\\you\\AnkusDrive\\.venv\\Scripts\\ankusdrive.exe",
# "args": ["mcp"] } } }Then restart the host; you should see the ankusdrive__* tools appear.
Supported Python: 3.10 – 3.14 (3.14 verified end-to-end on Windows 11 —
pip install, MCP stdio handshake, and ankusdrive ping → freecad=1.1.1). The script
checks your interpreter before pip runs, so a too-new CPython says so instead of
failing inside the resolver.
Optional solvers (SU2, Elmer, PrusaSlicer, WSL-backed OpenFOAM) come afterwards via
scripts\install-solvers.ps1. For the Linux-only solvers (OpenFOAM, FSI,
injection molding, YADE, openEMS, Bempp), run ankusdrive container setup --install-engine once WSL is installed. It runs them from the prebuilt image with
Docker inside WSL
(how). Full per-solver reality, the test suite, and the WSL2
route: docs/WINDOWS.md.
Ubuntu 24.04+ / containers (apt has no FreeCAD)
FreeCAD was dropped from Ubuntu 24.04's universe repo, so apt install freecad finds no candidate there, and upstream's snap/flatpak both fail in a
container or sandboxed agent environment (no snapd session, no FUSE). The path
that works everywhere is the official AppImage, extracted:
scripts/install-freecad-appimage.sh # or: scripts/install-solvers.sh freecadIt downloads the pinned release AppImage, checks its SHA-256, unpacks it with
--appimage-extract (a userspace squashfs unpack — no FUSE, no root, no
snapd, which is why it works in a container), symlinks freecadcmd, freecad,
ccx and gmsh into /usr/local/bin, and then live-verifies the result with
ankusdrive ping plus a real CalculiX solve (ankusdrive fem cantilever). Without a
writable /opt it installs to ~/.local/opt/freecad instead; --prefix /
--bindir override both, --appimage FILE reuses a download you already have.
The symlink step is optional: AnkusDrive also probes
/opt/freecad/squashfs-root/usr/bin (and ~/.local/opt/freecad*/…) directly, so
a hand-extracted AppImage in either prefix is auto-discovered. ccx and gmsh
ride along inside the AppImage, so structural FEM works off this one download.
Telling AnkusDrive where FreeCAD lives
AnkusDrive auto-discovers freecadcmd in this order: $ANKUSDRIVE_FREECADCMD,
then shutil.which(...) on PATH (trying freecadcmd, FreeCADCmd, and
freecad.cmd), then a per-OS list of standard install locations:
OS | Auto-discovered locations (newest version wins) |
macOS |
|
Linux |
|
Windows |
|
So a stock installer on any of the three needs no configuration. For a non-default install, point AnkusDrive at the binary directly:
export ANKUSDRIVE_FREECADCMD=/path/to/freecadcmd # macOS/Linux$env:ANKUSDRIVE_FREECADCMD = "D:\Apps\FreeCAD\bin\freecadcmd.exe" # Windowsankusdrive doctor prints which of the three layers (env / PATH / auto) actually
resolved FreeCAD, plus every candidate it checked — the fastest way to debug a
"FreeCAD not found" on a new box.
Drawing export (PDF/SVG)
export_drawing builds 2-D mechanical drawings (multi-view PDF/SVG/DXF with
dimensions) entirely headless. DXF uses FreeCAD's own writer and needs
nothing extra. PDF and SVG are composed and rasterised with reportlab +
svglib, and because that runs inside the worker — FreeCAD's bundled Python,
not the host venv — the two packages must be installed into FreeCAD's Python:
# Resolve FreeCAD's bundled Python from freecadcmd itself (portable across the
# macOS .app, a Linux distro package, and an extracted AppImage). freecadcmd
# prints a startup banner after the script output, so match a marker line
# rather than taking the last line:
printf 'import sys; print("DPREFIX="+sys.prefix)\n' > /tmp/_fcprefix.py
FREECAD_PREFIX="$(freecadcmd /tmp/_fcprefix.py 2>/dev/null | sed -n 's/^DPREFIX=//p')"
FREECAD_PY="$FREECAD_PREFIX/bin/python" # some builds: $FREECAD_PREFIX/bin/python3
# Pin svglib<1.6 — newer svglib pulls rlPyCairo -> pycairo, a native build we
# don't use (our drawings are line art, no gradients).
"$FREECAD_PY" -m pip install reportlab "svglib<1.6"
# Verify:
"$FREECAD_PY" -c "import reportlab, svglib; print('drawing export ready')"Without this, export_drawing still produces .dxf; .pdf/.svg raise a clear
ModuleNotFoundError. FreeCAD already bundles Pillow (reportlab needs it), so
no separate install is required.
Wiring it into an MCP host
The MCP server speaks stdio. Point your host at the ankusdrive binary and
let it run the mcp subcommand.
Claude Desktop — add to ~/Library/Application Support/Claude/claude_desktop_config.json
(macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):
{
"mcpServers": {
"ankusdrive": {
"command": "ankusdrive",
"args": ["mcp"]
}
}
}If ankusdrive isn't on the host process's PATH, use an absolute path —
e.g. /Users/<you>/.local/bin/ankusdrive (pipx default) or
/absolute/path/to/AnkusDrive/.venv/bin/ankusdrive (clone+venv).
Claude Desktop, one click — download ankusdrive-<version>.mcpb from the
latest release and open it.
Claude Desktop sets up its Python environment with uv, so no pipx step is needed
— FreeCAD 1.1 still is. The install dialog has one optional field, the FreeCAD
command path, for a FreeCAD that auto-discovery cannot find.
Claude Code — register once:
claude mcp add ankusdrive -- ankusdrive mcpOther hosts (Cursor, Continue, custom MCP clients) — same shape: stdio
transport, command = ankusdrive, args = ["mcp"].
After restarting the host, you should see 280+ ankusdrive__* tools become
available. If startup hangs or the host reports a closed connection, run
ankusdrive ping directly — that exercises the same worker boot path with
cleaner error messages.
Tool families (toolsets). Every tool definition takes up the client's context, and
all 283 come to roughly 114k tokens. Tools are grouped into families you can switch on
and off: core (always on), drawings, fem, components, sheet_metal, assembly,
intent, manufacturing, hand_calcs, simulation, plm, rendering.
pip / pipx / uvx / clone: every family is on unless you set
ANKUSDRIVE_TOOLSETS, e.g.ANKUSDRIVE_TOOLSETS=drawings,fem,simulation, ortoolsets = "..."inconfig.toml.Claude Desktop extension:
core,drawingsandfemare on by default (~31k tokens); turn others on in the extension's settings.
setup_status lists the families that are off and exactly how to enable each one.
run_script executes Python the agent writes, with full access to your files and
processes. It's controlled by ANKUSDRIVE_ALLOW_RUN_SCRIPT (env, or allow_run_script
in config.toml):
pip / pipx / uvx / clone: allowed unless set to
false.Claude Desktop extension: off by default; turn it on in the extension's settings.
When it's off, the tool isn't offered at all, and setup_status says how to enable it.
Simulation solvers & review-video demos
The base install (FreeCAD + pip install ankusdrive) covers geometry, the analytic
oracles, and the MCP surface. The heavy simulation families each shell out to an
external solver, discovered at runtime by ankusdrive/solvers.py
($ANKUSDRIVE_<SOLVER>_PATH → PATH → standard install dirs). A family whose solver is
absent degrades to a clean {ok: false, reason, install} dict instead of crashing — check
what currently resolves with ankusdrive doctor (cross-platform, no server boot needed),
the solve_capabilities MCP tool, or the install script's list. The install script
installs the pip-wheel solvers and provisions the native ones —
scripts/install-solvers.sh on Linux/macOS (apt/conda + source builds), and
scripts/install-solvers.ps1 on Windows (pip extras +
portable SU2/Elmer/PrusaSlicer downloads; CalculiX auto-detected from FreeCAD's bundle).
Persistent config: every ANKUSDRIVE_* path can instead live in
~/.config/ankusdrive/config.toml (%APPDATA%\ankusdrive\config.toml on Windows;
ANKUSDRIVE_CONFIG overrides): freecadcmd = "..." at top level, one lowercased key per
solver var under [solvers] (su2_path, elmer_path, openfoam_bashrc, ...). Env vars
still win when set; the file is the layer that survives an MCP host's minimal launch
environment. ankusdrive doctor reports the file and which layer resolved each value.
Where a solve's files go: every built-in solve writes its deck — a .sif plus
mesh, an OpenFOAM case tree, a .inp, a sliced .gcode — into its own directory under
<system temp>/ankusdrive-cases, and reports that directory as case_dir. They are
kept, because a result names them and a second tool is handed them (a warpage solve
consumes the cooling case a fill solve wrote), and they are reaped oldest-first once
the root passes 64 directories or 4 GB — never touching one written within the last
hour, so a running solve cannot be pulled out from under itself. solve_capabilities
reports the root and the live numbers under cases. Tune with ANKUSDRIVE_CASE_ROOT,
ANKUSDRIVE_CASE_KEEP, ANKUSDRIVE_CASE_MAX_GB, ANKUSDRIVE_CASE_GRACE_S, or turn
reaping off with ANKUSDRIVE_KEEP_SCRATCH=1. A case_dir you supply is never
touched, wherever it lives.
Every solve result also carries deck — a manifest of what the solver was handed,
taken the moment its first step launched, before it wrote any output into the same
directory: {digest, count, bytes, files: {path: hash}}. A session_transcript
compares it with s.deck(...) ahead of that solve's checks, so a replayed number that
drifted arrives already explained — ~ case.sif printed right above the failing check
means the problem changed; deck matches the recording means it did not, and the
solver or the environment did. CalculiX FEM results carry their .inp the same way.
Platform note: the solver discovery layer is fully cross-platform (per-OS install
dirs, Windows PATHEXT/.exe, env overrides), so ankusdrive doctor gives an honest report
on macOS/Linux/Windows. The pip-wheel families (MBD, topology, optics, fluids) install
identically everywhere. The native-binary families differ by OS — CalculiX ships inside
every FreeCAD install; SU2 and PrusaSlicer have good Windows/macOS binaries; Elmer has a
portable Windows zip but no macOS binaries; the
OpenFOAM-backed families (CFD, FSI, injection molding) — plus YADE, openEMS and
Bempp — rely on a Linux shell + linker glue. On macOS they run through the signed
solver container, native on Apple Silicon and with no source builds: see
macOS quickstart. On Windows they run
through WSL. See
docs/WINDOWS.md and docs/MACOS.md for the full
per-solver reality and setup on each OS.
The review-video demos under scratch/ turn a solver result into a GIF a human
can watch — the real exported geometry in motion with the matching oracle overlaid on
the frame (written to artifacts/). Each needs its family's solver plus matplotlib, and
the CFD one needs meshio (on top of the base numpy/Pillow):
pip install matplotlib meshio # frame rendering + reading OpenFOAM's VTK outputReview-video demo ( | Solver it drives | Install |
| PyBullet (pip wheel) |
|
| PyBullet (pip wheel) |
|
| CalculiX |
|
| Elmer |
|
| OpenFOAM + | OpenFOAM via apt/conda, then |
All of them also use FreeCAD for the geometry/meshing, so run each with the same
interpreter that launches the worker — e.g. .venv/bin/python3 scratch/cfd_field_video.py.
Optics
Two optics engines sit behind the MCP surface, in two licensing/runtime lanes:
Lane | Tools | Engine | Install |
Sequential — lens design + optimization |
| optiland / rayoptics (MIT/BSD, in-process) |
|
Non-sequential — tracing through STL solids |
| KrakenOS (GPL-3.0, out-of-process only) |
|
The sequential engines import in-process, so install the optics extra into the same
interpreter that launches the worker (like the other wheels). The non-sequential engine
is GPL-3.0 and is therefore never imported by AnkusDrive — it runs in a separate
subprocess (ankusdrive/optics_gpl_runner.py), the same
arm's-length boundary used for the GPL Elmer/OpenFOAM binaries. The worker locates a
Python that can import KrakenOS automatically (from where the wheel is installed); override
with ANKUSDRIVE_OPTICS_GPL_PYTHON=/path/to/python. Because of that isolation the GPL extra
is opt-in: the no-argument install-solvers.sh run installs only the permissive
extras and prints how to add optics_gpl. Rendered examples for both lanes (lens layout,
spot diagram, optimization, prism TIR, and a ball-lens spherical-aberration study) live in
examples/optics/optics_gallery/ — regenerate with
.venv/bin/python examples/optics/optics_gallery.py (and …_3d.py, optics_ball_lens.py), or
bootstrap everything in one shot (installs both lanes, then renders every figure):
scripts/install-solvers.sh --optics-galleryArchitecture sketch
┌────────────┐ ┌────────────┐ ┌──────────────────────┐
│ MCP host │ ───► │ AnkusDrive │ ───► │ freecadcmd worker │
│ (Claude) │ │ (Python) │ IPC │ (long-lived Python) │
└────────────┘ └────────────┘ └──────────────────────┘
▲ ▲ │
│ │ ▼
└── CLI user ────────┘ .FCStd / .inp / .vtkKey decision: long-lived worker with JSON-over-stdin/stdout, not subprocess-per-call. FreeCAD startup is ~1–2s; re-paying that per tool call is unacceptable for an interactive agent. The worker is a small Python loop launched under freecadcmd, reading commands, dispatching to handlers, returning structured results (including object IDs so follow-up calls can reference created geometry).
FreeCAD API surface we care about
Notes gathered from the scripting docs and the FEM Python tutorial:
Core (App):
App.newDocument(name)/App.ActiveDocument/doc.recompute()/doc.save(path)doc.addObject("Part::Box", "name")— typed object creation; properties set after (box.Height = 5)doc.supportedTypes()for introspection;obj.TypeId,obj.isDerivedFrom("Part::Feature")
Modeling:
Part—makeBox,makeCylinder,makeSphere, booleancut/common/fuse, fillets, lofts (OpenCASCADE under the hood)Draft— 2D primitives,move, arraysSketcher+PartDesign— parametric sketch-driven solids (most "real" mechanical design happens here)FreeCAD.Vector,Placementfor positioning
FEM (ObjectsFem + femtools):
ObjectsFem.makeAnalysis(doc, "Analysis")— containermakeSolverCalculixCcxTools/makeSolverElmer— solver objects with tunables (GeometricalNonlinearity,ThermoMechSteadyState, …)makeMaterialSolid— assignYoungsModulus,PoissonRatio,DensityConstraints:
makeConstraintFixed,makeConstraintForce,makeConstraintPressure,makeConstraintDisplacement, contact/tie/spring, thermalMesh:
makeMeshGmsh+femmesh.gmshtools.GmshTools(...).create_mesh()(or Netgen)Run:
femtools.ccxtools.FemToolsCcx().run()Results: iterate
analysis.GroupforFem::FemResultObject; read.DisplacementVectors, stress fields
Headless invocation:
freecadcmd script.py— runs script then exitsfreecadcmdwith no args — interactive Python REPL (what the worker will drive)--console,-M <moddir>,-P <pypath>,--pass <args>,FreeCAD.ConfigGet(...)for env infoFreeCADGuiis not available headless — keep design logic inApp/Part/Femonly
How an agent reaches FreeCAD: three layers
AnkusDrive exposes FreeCAD through three layers, each with a different audience and a different cost-of-use. Knowing which layer a feature lives in tells you how to invoke it.
Layer 1 — typed MCP tools (the agent surface)
280+ first-class MCP tools span the core mechanical-design surface, a broad engineering-analysis / simulation surface, and a design-control (PLM) layer. They have validated parameters, structured returns, and stable handles for chaining. This is the happy path — what an agent uses for things people do every day.
Domain | What's covered |
Document lifecycle |
|
Geometry primitives |
|
Selection (stable refs) |
|
PartDesign |
|
Direct modeling & feature ops |
|
Parametric components |
|
Metrology & inspection |
|
Generic property access |
|
Functional intent & invariants |
|
Performance contracts |
|
Design-space studies (DOE) |
|
Optimize to a spec |
|
Assembly & interfaces |
|
Drawings (TechDraw, headless) |
|
Inspection (first-article) |
|
Release packages (vendor / RFQ) |
|
Off-the-shelf parts (buyability) |
|
Visual feedback |
|
FEM (FreeCAD/CalculiX/Elmer) |
|
Engineering oracles & hand-calcs | machine elements ( |
Simulation families (external solvers, async) | screens + full solves that shell out to OpenFOAM/Elmer/CalculiX/openEMS/YADE/KrakenOS, most via a submit→poll job pattern: thermal/CHT ( |
Async jobs |
|
Materials & fluids |
|
Sheet metal |
|
Manufacturing & Design-for-X |
|
CNC (machinability + machining time) |
|
Tolerance ↔ cost |
|
Design control / PLM | items & part numbers ( |
Operations |
|
Session transcripts |
|
All tools return JSON; geometry-creating tools return a handle (e.g.
pad_1) that subsequent calls reference. The heavy simulation families return
a {ok: false, reason, install} dict (rather than crashing) when their solver
isn't installed — see Simulation solvers.
Layer 2 — generic property reflection
For the long tail of "I just need to tweak this one property" without a dedicated tool:
get_object(handle)— dump every entry inobj.PropertiesListwith Quantities → float (mm/deg), Vectors → list, Placements → dict.set_property(handle, name, value)— set any single property by name.
Use this when a typed tool exists for the object kind but doesn't expose the
exact property you need (e.g. Refine on a Pad, Sections ordering on a
Loft, internal tunables on a CCX solver).
Layer 3 — run_script (the universal escape hatch)
For features that have no first-class MCP tool at all — e.g. Path workbench (CAM toolpaths), Surface workbench, Arch/BIM, Spreadsheet, TechDraw dimensions, contact/spring FEM constraints, B-spline sketcher operations, expression-engine bindings, anything in a workbench AnkusDrive doesn't wrap.
run_script(code='''
import Path
job = Path.Job.Create("Job", [_resolve("pad_1")])
__result__ = {"job_name": job.Name}
''')Inside the script, the worker pre-injects: App / FreeCAD, Part,
ObjectsFem, plus _register(prefix, obj) / _resolve(handle) /
_handles so scripts can register new objects into the same handle
registry that typed tools use. Set __result__ = ... to a JSON-serializable
value to return data; print statements go to /dev/null.
The escape hatch costs more (the agent has to write FreeCAD Python) but makes the entire FreeCAD API reachable. The Phase 2 plan's "After Phase 2" section calls out which run_script patterns deserve promotion to typed tools — that's how the surface grows over time.
Regenerating a session
session_transcript() returns the session so far as a script:
with Session() as s:
r2 = s.add_primitive(kind='box', w=40.0, d=20.0, h=5.0)
box_1 = r2['handle']
r3 = s.add_primitive(kind='cylinder', h=5.0, r=3.0)
cylinder_1 = r3['handle']
r4 = s.boolean_op(op='cut', base=box_1, tool=cylinder_1)
s.check(r4, 'volume', 3964.657082647114)
s.save_document(path=str(WORKDIR / 'bracket.FCStd'))Save it and run python transcript.py [WORKDIR] to rebuild the session in a fresh
worker. ankusdrive.replay.Session calls the same functions the MCP server serves,
so a transcript can call no tool the server doesn't have. run_script stays behind
its switch, and a missing solver stops the run at that step. Each s.check() stops
the run at the first result that differs from the recording. The script's header
lists what it can't reproduce: failed calls, run_script code, and files the session
read, which you copy into WORKDIR first.
Auditing an analysis
The same tool answers the other question a transcript is for: what exactly produced
this number? A simulation session exports with its derivation intact — the closed-form
estimates are not dropped as "inspection", every number an analysis or a solve reported
becomes an s.check(), and the verdict fields become s.expect(), so a replay that
reaches a different solver or falls back to a different correlation stops there rather
than returning a plausible figure:
PROVENANCE = {'env': {'ankusdrive': '0.5.5', 'freecad': {'version': '1.1.0'},
'substrate': 'container', ...},
'solvers': {'elmer': {'version': '26.2', 'via': 'container',
'path': '/usr/bin/ElmerSolver', ...}}}
with Session() as s:
s.provenance(PROVENANCE) # prints every difference from the recording
r1 = s.h_estimate(geometry='vertical_plate', characteristic_mm=100.0, t_surface_c=200.0)
s.check(r1, 'h_total_w_m2k', 8.4111, rel=0.001)
s.expect(r1, 'correlation', 'churchill_chu_vertical_plate')
r2 = s.thermal_transient_submit(half_thickness_mm=1.5, h_conv=8.0, duration_s=600.0, ...)
r5 = s.wait(r2['job_id'])
s.check(r5, ('result', 't_center_c'), 45.49120518934, rel=0.001)
s.expect(r5, ('result', 'solver'), 'elmer')s.provenance() re-resolves the whole environment on the replaying machine and prints
what moved: a different Elmer version, a solver that relocated when the substrate
changed, a FreeCAD that is not the one that built the model. A replay on a different
solver is not a failure — it is the finding. Solver paths under $HOME are collapsed
to ~/…, so the record says which install without saying who. session_transcript
also returns the record as provenance for attaching to a report; pass
provenance=False to skip it and the version probes it runs.
Keeping the record: the durable journal
A transcript lives as long as the server does. For analysis that feeds a design decision, turn on the durable journal — it is off by default, and one setting enables it:
export ANKUSDRIVE_JOURNAL_DIR=~/ankusdrive-journal # or journal_dir = "..." in config.tomlEvery tool call is then appended to session-<start>-<pid>-<id>.jsonl in that
directory: a header with the environment (AnkusDrive / Python / platform / substrate),
the FreeCAD each worker booted, and one line per call — arguments whole (run_script
code verbatim with its SHA-256), the trimmed result, the solver each solve resolved
to, and result_digest, a SHA-256 over the full result, so a result the journal
had to trim is still pinned. Writes are best-effort: a journal that cannot be written
logs a warning and never fails or changes a tool call.
After the server has exited, the file turns back into what session_transcript
would have returned — replay script, recorded environment, ordered call ledger with
digests:
ankusdrive journal list
ankusdrive journal export latest -o transcript.py # --json for the whole recordor, from an agent, journal_export(session="latest") (omit session to list).
Retention is stated, not left to the OS: at most 50 session files and 512 MB
(ANKUSDRIVE_JOURNAL_KEEP, ANKUSDRIVE_JOURNAL_MAX_MB), oldest first, never the live
session's file or one written in the last hour (ANKUSDRIVE_JOURNAL_GRACE_S); one
session past 64 MB (ANKUSDRIVE_JOURNAL_FILE_MAX_MB) keeps arguments and digests but
drops result bodies. ANKUSDRIVE_JOURNAL_REDACT=1 hashes paths and names (document
names, labels, title-block fields) in arguments, results and errors — never
run_script code or handles. The trade-off: a redacted journal is auditable by
digest, but the script it exports is not runnable.
To make the record travel with the model, save with
save_document(path, attach_provenance=True) (off by default). The document then
carries the same record — replay script, environment and solver identities, ledger
with each result's SHA-256 — in its Meta map, which survives FreeCAD re-saving the
file. It covers the calls that built that document: the saving workspace's current
worker, from the new_document / open_document that produced it, while it was the
active document, successful calls only. ANKUSDRIVE_JOURNAL_REDACT applies to it too.
A save without the flag removes an earlier record. Read it back without FreeCAD:
ankusdrive journal export bracket.FCStd -o transcript.py # --json for the whole recordWhat the CLI is (and isn't)
The CLI is not the agent surface — it's a human-debugging + transport tool. Seven subcommands:
Command | Purpose |
| Health check — boot a worker, prove FreeCAD is reachable |
| Single-shot primitive → .FCStd (manual smoke tests) |
| Headless format conversion |
| Execute arbitrary FreeCAD Python in a live worker (set |
| Start the MCP server over stdio — this is how an MCP host launches AnkusDrive |
| Run the built-in canned demo |
| Read the opt-in durable journal: list past sessions, export one as a replay script + provenance record |
Agents do not invoke the CLI. They speak MCP via stdio after the host has
launched ankusdrive mcp. The CLI's job is (a) to start that server and
(b) to give a human a way to poke at the worker without writing an MCP
client.
Decision rule
Need | Use |
Standard CAD/FEM operation | First-class MCP tool (Layer 1) |
Tool exists but I need property X |
|
Workbench / API not wrapped at all |
|
Smoke test from a shell, or stand up MCP | CLI |
Multi-agent design
The roadmap above is about deepening what one agent can do. The
orchestration/ layer is about many agents sharing the
work: split a product into components and subassemblies, build those in
parallel (each agent cold, seeing only its own contract slice), then merge the
whole back up with the joints actually fitting. The design is written up in
docs/MULTI_AGENT.md; it targets partition + merge,
not shared co-editing of one live document (a single worker = one
App.ActiveDocument, so concurrent mutation is a non-goal for now).
Concurrent agents on one MCP server — workspaces. FastMCP runs sync tools in
a thread pool, so a host can have several tool calls in flight at once. The
server keeps a pool of named workspaces, each its own freecadcmd process
with its own App.ActiveDocument and handle registry. Each concurrent agent
passes its own workspace=<name> on every tool call, which pins that call to
its workspace (created on first use); handles and documents do not cross
workspaces. Don't use use_workspace for this: its selection is server-wide, so
one agent's switch retargets everyone's calls (Claude Code subagents, for one,
share their parent session's server). A single agent may call use_workspace once
instead; a client that does neither sees the historical single-worker behavior
byte-for-byte (everything routes to the default workspace). Worker.call() is
internally serialized so two threads can never interleave the stdin/stdout
protocol on one process. The pool is capped (ANKUSDRIVE_MAX_WORKSPACES, default
4 including default) and idle workspaces are reaped (ANKUSDRIVE_WORKSPACE_IDLE_S, default 900s) so abandoned
sessions don't leak processes; list_workspaces / close_workspace manage it.
The split of responsibilities is deliberate:
AnkusDrive ships the thin, tool-agnostic primitives that make a merge verifiable —
publish_interface(declare a component's mating frames),merge_assembly(combine component files into one assembly), and the gates that decide whether a merge is sound:interface_align_check(do published frames line up?),interference_check(do solids collide?),envelope_check(does it fit its bounding budget?), plus anassembly_lock/assembly_lock_checkcontract lockfile. These are real MCP tools usable by any host.orchestration/is the host-side reference coordinator — explicitly not part of theankusdrivepackage. Given a free-text brief itdecomposes it into a validated manifest, fans out one builder agent per component,merge_assemblys them, reads the gates, and on failure renegotiates — re-dispatching only the components implicated by the failing gate — up to a round budget. It runs against a real Anthropic client or a scripted stub (ScriptedClient) for free dry runs; the merge and gates are real worker calls either way. Any host (Claude, another tool, a human) can use it, replace it, or ignore it — the only contract that matters is the manifest + the component files on disk.
How well partition+merge holds up is measured by a dedicated eval ladder
(tests/test_multiagent_m1.py / _m2.py, runnable in CI) with hard-oracle
merge gates and a single-agent baseline — see
tests/MULTI_AGENT_EVAL.md. Early experiments have
partition performing at or above the single-agent baseline on the harder toys.
Designs, not just parts — the design-control layer
Multi-agent orchestration partitions one product across a team. A separate
axis makes a design (not just a part) something you can parameterize, vary,
and evolve under control — the mechanisms a PLM/PDM workflow expects, mapped
onto AnkusDrive's deterministic, headless, git-diffable grain. The keystone
insight: the build recipe is the feature tree; the parameters are its inputs;
regeneration is re-running the recipe — so AnkusDrive gets parametric regen and
family tables without a live in-file expression engine. The full scoping and
rationale is in docs/DESIGN_HIERARCHY.md; the
agent-facing judgment lives in the design-modularly
skill.
Parametric hierarchy — recipes (
recipe,recipe_validate) are named, declared-input build templates (AnkusDrive's PowerCopy/UDF and its intra-part parametric model); a relations DAG drives driven dimensions from master parameters by formula (pitch_d = module * teeth, arithmetic only — no iterative solve, no double-driving); feature templates (feature_instantiate) graft reusable features onto reference geometry by name; the typedunitslayer rejects dimensionally-wrong inputs at the door ("5 N"for a length is an error, not a silent mis-scale).Variant families —
family_materializeexpands a row × column design table into a set of variants deterministically, running the recipe per row and allocating part numbers in table order.Identity & lifecycle — items (
items_new) give a part a stable part-number identity decoupled from its file path; a lifecycle state machine (lifecycle_transition: in_work → in_review → released → obsolete) enforces released-immutability, and a Form/Fit/Function predicate decides revision bump vs. new part number on a change.Change control — ECOs (
eco_create) are first-class change records;where_used/change_impactcompute blast radius over the dependency graph before you commit;baseline_create/baseline_verifypin reproducible snapshots.Versioned interfaces — an interface-type registry (
get_interface,nema17_face@1-style named/versioned types) plus a Liskovsubstitutability_checkgate enforce Form/Fit/Function compatibility as code, so a swapped part is verified to actually mate.Projects —
scaffold_project+project_validate/project_check_referencespromote the multi-agent directory convention to a first-classproject.json(manifest-of-manifests) with a master/skeleton single-source-of-truth slot and reference-integrity guards.
Like the merge gates, these are thin, deterministic, mostly FreeCAD-free primitives — the logic layers import and test without launching a worker.
Status
Phase 3 closed 2026-05-10 (v0.3.0). The core mechanical-design surface from Phase 2 (2026-04-25) is intact; Phase 3 layered intent-encoding APIs on top of it. Since then the tool surface has grown from ~100 to 280+ tools across several waves: a command-tier expansion (parametric components + direct feature ops + metrology), the multi-agent orchestration layer, a broad engineering-analysis + external-solver simulation surface (thermal/CFD/CHT/ EM/acoustics/FSI/molding/granular/optics/multibody), and a design-control (PLM) layer (items, recipes, variant families, lifecycle, ECO/change, versioned interfaces, projects).
Worker + transport — long-lived
freecadcmdworker, newline-JSON over stdio with stdio hygiene (FreeCAD C++ chatter redirected off the protocol fd).CLI —
ping,version,box,cylinder,export,run,mcp,fem cantilever, plus top-level--version.MCP server — FastMCP over stdio, 280+ typed tools across document lifecycle, primitives, selection (face/edge tags), full PartDesign (sketcher + pad/pocket/revolve/hole/loft/sweep/helix/fillet/chamfer/pattern/mirror/thickness/draft), direct-modeling feature ops, parametric components, metrology/inspection, generic property reflection, mass properties, assembly + interface gates, TechDraw (incl. headless PDF/SVG/DXF export, dimensions, gates), multi-view + photoreal rendering, FEM (static + modal + buckling + thermal + nonlinear + result-probe), the engineering-analysis oracles and external-solver simulation families (sync + async
*_submit/job_*), the materials/fluids corpora, Design-for-X / manufacturing checks, the design-control (PLM) layer, and transactions.Command tiers 1–3 — 21 new tools: parametric components (
add_gear,add_rack,add_sprocket,add_pulley,add_spring,add_fastener,add_bearing,add_thread), direct feature ops (fillet_edges,chamfer_edges,shell_solid,add_rib,engrave_text,oring_groove,transform,scale_shape,copy_shape), and metrology/inspection (measure_distance,measure_angle,bounding_box,check_shape,section_view,min_clearance).Multi-agent orchestration — AnkusDrive ships the thin merge primitives + gates (
publish_interface,merge_assembly,interface_align_check,envelope_check,assembly_lock/_check); the host-side reference coordinator (orchestration/) decomposes a brief, fans out per-component builders, merges, gates, and renegotiates. See Multi-agent design anddocs/MULTI_AGENT.md.Phase 3 intent-encoding additions —
direction='into_body'|'away_from_body'andthrough='wall'|'body'on pocket/hole (ray-cast wall depth handles hollow shells correctly);intended_for='print'|'machine'|'drawing'on hole drives ModelThread;verify_featurediffs actual-vs-expected volume change to catch silent failures; visibility hygiene at save hides consumed inputs;register_handle+run_scriptauto_register close the escape-hatch one-way trapdoor;list_thread_optionssurfaces the coupled ThreadType/ThreadSize enums dynamically; revolve has an OCCT pre-check that flags axis-coincident edges with an actionable error.Selection layer —
list_faces/list_edges/query_faces/resolve_*produce stable geometric tags that survive edits; FEM constraints take tags directly.Rendering — host-side software rasterizer (
ankusdrive/render.py) with per-pixel z-buffer (render_view/render_viewsreturn PNGs as MCPImageContent), plus photorealrender_photorealvia the FreeCAD Render addon + an external renderer (POV-Ray / LuxCore / Appleseed / Cycles / OSPRay / PBRT). Support matrix, install, and limitations:docs/RENDERING.md.Simulation surface — engineering-analysis oracles (machine elements, structural, durability, thermal, tolerance/GD&T) plus external-solver families that shell out to OpenFOAM / Elmer / CalculiX / openEMS / YADE / KrakenOS, discovered at runtime by
ankusdrive/solvers.pyand degrading cleanly when absent. Long solves use an async submit→poll job pattern (*_submit+job_status/job_result/job_list). Catalog and result schemas:docs/SIMULATION_TOOLS.md; proof harness:docs/SIMULATION_EXAMPLES.md. Materials/fluids back these viamaterial_*andfluid_props(mechanical-property / molding / CoolProp corpora).Design-control (PLM) layer — recipes + a relations DAG (parametric regen), feature templates, variant families from a design table, item/part-number identity, a lifecycle/revision state machine, ECO change records with where-used/impact + baselines, a versioned interface registry + Liskov substitutability gate, and project containers with reference-integrity guards. Scoping + rationale:
docs/DESIGN_HIERARCHY.md. See Designs, not just parts.Tests — ~980 test functions across ~90 files (worker / MCP / CLI / render / determinism / edit stability / negative paths / perf / multi-agent / simulation families / molding / PLM layer), runnable via
tests/run_all.sh(Linux/macOS) ortests/run_all.ps1(Windows — single-interpreter, skips the Linux-only solver families; seedocs/WINDOWS.md). Reliability harness (Layer A classification, B diff-detection, C agent-loop closure) is gated behindRUN_RELIABILITY=1; seetests/RELIABILITY.md.
See docs/ROADMAP.md for the Phase 1/2 per-slice record — a
changelog of how the surface above was built, frozen at the close of Phase 2. Open
work (FEM contact/spring/tie refinements, feature_tree
introspection, deeper external-solver integrations) lives on the
issue tracker, not in that file.
The committed showcase — every GIF, render, drawing and exported solid the docs point
at, with the script that regenerates each one — is indexed in
artifacts/README.md.
Open questions
Error model: FreeCAD raises plain Python exceptions from C++; worker catches and serializes them, but stack context across the JSON boundary is still lossy.
Async / concurrency: multi-doc shipped (
list_documents/set_active_document/close_document), and every long-running solve — CalculiX included, viafem_run_submit— can run off the channel through the*_submit+job_*pattern. The synchronousfem_runremains for small solves. One worker still means one main thread: a job's FreeCAD-side steps (writing the solver input, importing results) run on it, between requests.
Privacy Policy
AnkusDrive runs entirely on your computer and collects nothing: no telemetry, no
analytics, no accounts, and no network requests from its own code. It reads and writes
only the files you point it at, plus its config file and temp working directories. Your
MCP host (e.g. Claude Desktop) sends tool inputs and results to its AI model provider
under the host's own policy. The full policy, including exactly what is stored where, is
in PRIVACY.md.
Evaluating the Claude Desktop extension? The reviewer guide covers installation, a setup check, and three example prompts with expected results.
License
Licensed under the Apache License, Version 2.0. Contributions submitted to this project are licensed under the same terms (Apache 2.0 §5: inbound = outbound), which means contributors retain copyright but grant the project — and everyone downstream — a perpetual, irrevocable license to use their work, including a patent grant. The intent is to keep the project welcoming to contributors while ensuring nobody can later re-proprietize what they contributed.
The code is Apache-2.0; the name is not. Apache-2.0 §6 grants no trademark
rights, so the AnkusDrive word mark and the brand assets in
logo/
are covered separately — see
TRADEMARKS.md
for what you may do without asking (referring to the project, compatibility
claims, redistribution, packaging, and forking all qualify) and
NOTICE for the
attribution a redistributor must carry.
References
Available Tools
286 toolsacoustic_fem_submitAcoustic FEM SubmitADestructive
Acoustic FEM via Elmer HelmholtzSolve (SIMULATION_NEXT Tier B1),
asynchronous — the higher-order twin of acoustic_screen, gated against its
exact closed forms. Requires ElmerSolver; when absent this returns {ok:false,
reason, install} rather than raising.
kind='duct': a closed duct driven p=1 at x=0, rigid at x=L, at f = kL·c/(2πL)
(keep kl off the quarter-wave resonances) — the rigid-end pressure has the
exact oracle 1/cos(kL), so p_end_ratio ≈ 1 machine-tight. kind='cavity': a
rigid lx_m × ly_m cavity excited by a corner Wave Flux source, swept ±span_pct%
around the exact (mode_nx, mode_ny) eigenfrequency in n_steps Scanning steps;
the in-phase corner-probe response flips sign through resonance, and the 1/A
zero-crossing gives f_solved_hz with mode_ratio ≈ 1 (<0.1%). Also accepts
a prepared case_dir.
Returns the degradation dict or {job_id, status, cache_hit}; poll job_result for duct {ok, p_end_re, p_end_exact, p_end_ratio (≈1), p_mean_ratio, frequency_hz, case_dir} | cavity {ok, f_solved_hz, f_exact_hz, mode_ratio (≈1), mode, case_dir}.
Inputs: kl, nx, ny, sif, +12 more.
acoustic_radiation_submitAcoustic Radiation SubmitA
Exterior-acoustics boundary-element solve on Bempp, asynchronous (OFF the MCP
channel) — the real-field twin of the analytic monopole_sphere /
rigid_sphere_scattering oracles. Bempp is MIT but needs meshio>=4 (clashing
with solidspy's meshio==3 in the shared venv), so it is run ONLY out-of-process
via ankusdrive/bempp_runner.py under a dedicated .venv-bempp; degrades to
{ok:false, reason, install} when no bempp venv resolves.
problem='radiation' (default): a pulsating (monopole) sphere of radius a_m,
uniform surface velocity u_amp at freq_hz, into air (rho,c), mesh size
h (fraction of a). The result's radiated_power_w / farfield_pressure_x_r
vs the monopole_sphere oracle (ratio≈1) IS the gate. problem='scattering': a
rigid sphere insonified by a unit plane wave; sweep ka_list, report the
far-field form function at theta_deg angles (h_per_wl elements/wavelength) —
gated against the rigid_sphere_scattering Mie oracle. problem='mesh_solve': a
radiation solve on a REAL FreeCAD model (handle), tessellated to a surface
mesh here and fed to the BEM engine (scale_to_m mm→m, linear_deflection
mesh tolerance).
Returns the degradation dict, or {job_id, status, cache_hit}; poll job_result for {ok, ka, radiated_power_w, farfield_pressure_x_r, surface_pressure_abs_mean, n_elements, wall_s} (radiation/mesh_solve) or {results:[{ka, form_function_abs{}, backscatter_abs, n_elements}]} (scattering).
Inputs: c, h, a_m, r_m, +12 more.
acoustic_screenAcoustic ScreenARead-only
Closed-form acoustics screen (NO solver). kind: 'cavity_modes' (lx/ly/lz_mm
-> the lowest n_modes rigid-cavity eigenfrequencies f=(c/2)·√(Σ(n/L)²) with
[nx,ny,nz] indices — exact, and the future oracle for the planned Elmer
HelmholtzSolve FEM) | 'helmholtz' (neck_area_mm2 + neck_length_mm +
cavity_volume_mm3 -> resonance with flanged end correction — ±10 %) |
'mass_law' (frequency_hz + surface_density_kg_m2 -> limp-wall TL =
20·log₁₀(f·m″)−47 dB — ±3 dB) | 'duct_cutoff' (duct_width_mm or
duct_diameter_mm -> first cross-mode; plane waves only below — exact). Sound
speed from air at t_ambient_c unless c_m_s given. Fidelity is labeled per kind;
escalate to the Elmer acoustic_fem_submit solve (Tier B1) when the margin is
within ~2× the band.
Returns {kind, c_m_s, fidelity, band_pct, band_db, valid_range_ok, warnings, escalate_to} plus per kind: {modes:[{f_hz,n}], f_fundamental_hz} | {f_resonance_hz, neck_radius_mm, l_eff_mm} | {tl_db, fm_product} | {f_cutoff_hz, geometry}.
Inputs: kind (required), c_m_s, lx_mm, ly_mm, +11 more.
add_annotationAdd AnnotationA
Add a free text annotation to a drawing page at page position (x, y) in mm (origin bottom-left, +Y up, matching TechDraw view placement). Returns {handle, name, text}.
Inputs: page (required), text (required), x, y, +2 more.
add_bearingAdd BearingA
Add a deep-groove ball bearing as an assembly envelope solid: an annular ring (outer-diameter cylinder minus bore cylinder) of the given width, axis along +Z. Balls/races are not modeled — this is the fit envelope a coordinator needs to size the shaft, the housing bore, and the shoulder spacing.
Specify dimensions ONE of two ways:
designation: a standard metric series code, looked up in a built-in table. Known: "608", "623", "624", "625", "626", "688", "6000", "6200", "6800", "6900". (e.g. "608" -> bore 8, OD 22, width 7 mm.)
bore + outer_diameter + width: explicit dims in mm (all three required). Explicit values override a designation's table values when both are given.
bore: inner-bore diameter mm (sizes the shaft). outer_diameter: OD mm (sizes the housing bore). width: axial length mm (shoulder spacing). placement: optional [x, y, z] mm translation of the bearing's near face.
seals: open (default) | RS | 2RS | RZ | 2RZ | Z | 2Z. It changes no geometry — the envelope is identical — but it IS part of the orderable identity: 608, 608-2Z and 608-2RS are three different purchases with different drag, speed limits and prices. It is folded into the canonical designation stamped on the part ("608-2RS").
A bearing built from raw bore/OD/width with NO designation is a dimensional envelope, not a purchasable part, and is deliberately left undesignated rather than given a made-up catalog number.
Raises ValueError if the designation is unknown and dims are incomplete, or if outer_diameter <= bore.
Returns {handle, name, designation, bore, outer_diameter, width, volume,
orderable, catalog}. handle starts "bearing_". designation is None when built
from explicit dims; orderable is the designation card, whose own designation
carries the seal suffix; catalog is the off-the-shelf verdict (stocked or not,
None for an undesignated envelope).
Inputs: bore, name, seals, width, +4 more.
add_dimensionAdd DimensionA
Add dimension(s) to a drawing page. Modes (pick one):
auto=True: overall horizontal + vertical extents for every part-view (or only
views, by name or projection code).view + edge=: true length of a model edge (the real length, not the foreshortened projection).
view + kind='diameter'|'radius' + edge=: ⌀/R of a hole or arc.
view + kind='angle' + face=: included angle (2×) of a cone/countersink/taper; half_angle=True for the half-angle.
view + from_point/to_point ([x,y,z] model points): point to point. view: a view handle, object name, or projection code ('Front', 'Top', ...). kind: 'aligned' (default) | 'horizontal' | 'vertical' | 'diameter' | 'radius' | 'angle'. tolerance: rendered next to the value: {"sym": 0.1} for ±0.1, {"plus": .., "minus": ..}, or {"fit": "H7"} / {"fit": "H7/g6"} (ISO 286 hole-side limits at the basic size). Returns {dimensions: [{handle, name, type, value}, ...]}; value is the true measured size.
Inputs: page (required), auto, edge, face, +8 more.
add_fastenerAdd FastenerA
Add a standard ISO metric fastener (screw / bolt / nut / washer) as a solid.
kind: one of "socket_head_cap_screw", "hex_bolt", "hex_nut", "washer".
socket_head_cap_screw: cylindrical head with a cosmetic hex socket + plain shank (threads not modeled).
hex_bolt: hex head (across-flats) + plain shank.
hex_nut: hex prism with an axial clearance hole.
washer: flat annular ring. size: ISO designation, one of "M3","M4","M5","M6","M8","M10","M12". length: shank length in mm. REQUIRED for socket_head_cap_screw and hex_bolt; ignored for nut/washer. grade: optional material / property class AS ORDERED — ISO 898-1 for steel screws ("8.8", "12.9"), ISO 898-2 for nuts ("8", "10"), ISO 3506 for stainless ("A2", "A4-80"). It changes no geometry. What it changes is the ORDERABLE designation stamped on the part: with it you get "ISO 4762 M4×12 A2", which a buyer can quote; without it the designation comes back complete=False saying nobody has chosen between class 8.8 steel and A2 stainless yet. An unrecognised grade is a loud error, never a guess. placement: optional [x, y, z] mm translation of the fastener origin (head top sits at z=0, shank runs in -z for screws/bolts). name: optional object name (default derived from kind).
All dimensions are in mm. Threads are cosmetic (the shank is a plain cylinder of the major diameter).
Returns {handle, name, kind, size, major_diameter, pitch, volume,
designation, orderable} plus, by kind:
screws/bolts add {length, head_diameter, head_height, model_thread:false};
nut adds {head_diameter (wrench across-flats), head_height};
washer adds {head_diameter (outer diameter), head_height (thickness)}.
Mating numbers: drill a through-hole of major_diameter (+ clearance) for the
shank; head_diameter sizes a counterbore. designation is the canonical
orderable identity ("ISO 4762 M4×12 A2") and orderable its full card.
catalog is the off-the-shelf verdict computed at creation time — code
stocked, or not_stocked naming the lengths either side. A length nobody
stocks is a FINDING, not a refusal: the solid is still built, so you can decide
whether to move the stack-up or accept a special.
Inputs: kind (required), size (required), name, grade, +3 more.
add_feature_noteAdd Feature NoteA
Attach an explicit manufacturing NOTE that satisfies a curved/periodic
feature the drawing_gate enumerates (issue #108) — the 'per CAD model / profile
table' coverage for geometry a single number can't capture (a freeform/BSpline
wall's profile, a tooth pattern's full parameter set) or a documented cone angle.
feature is the enumerated feature id (e.g. 'FREEFORM1', 'PAT1', 'CONE1', from
drawing_gate's enumerated_features); text defaults to a sensible callout.
The gate reads the note back as coverage. Returns {handle, name, feature, text}.
Inputs: page (required), feature (required), x, y, +4 more.
add_gdt_calloutAdd GD&T CalloutA
Place a GD&T feature control frame on a drawing page, so the tolerance is declared on the print: it renders as a compartmented symbol and inspection_plan / fai_report read it back as a ballooned characteristic.
control: ASME Y14.5 characteristic, as gdt_check accepts: flatness,
straightness, circularity, cylindricity, profile_line, profile_surface,
perpendicularity, parallelism, angularity, position, concentricity, runout,
total_runout.
zone: tolerance zone mm (Ø-prefixed for position, concentricity, circularity,
cylindricity).
datums: ordered datum reference frame, e.g. ["A", "B", "C"]. With datums the
plan routes it to CMM; datum-free form controls go to the surface plate.
feature: optional enumerated feature id (drawing_gate enumerated_features).
mmc_bonus: material-condition bonus tolerance, mm.
modifier: free text in the tolerance compartment (e.g. "Ⓜ").
x / y: page position mm (origin bottom-left, +Y up).
Returns {handle, name, control, zone, datums, text}.
Inputs: page (required), zone (required), control (required), x, +8 more.
add_gearAdd GearA
Add an involute spur gear (FreeCAD's core involute generator), extruded to a solid.
teeth: tooth count (>= 3). module: mm (pitch diameter = module * teeth). height: extrusion thickness mm. pressure_angle: deg (default 20). external: True for an external gear; False for an internal/ring tooth profile. placement: optional [x, y, z] mm translation. Returns {handle, name, volume, pitch_radius, tip_radius, root_radius, teeth, module, external}. Two external gears MESH when their axes are spaced (pitch_radius_a + pitch_radius_b) apart; phase one by half a tooth to avoid tooth-on-tooth interference.
Inputs: teeth (required), module (required), name, height, +4 more.
add_partAdd PartA
Add a part to an assembly via App::Link.
source is one of: {"handle": ""} — link an in-doc body {"path": "/path/to/part.FCStd"} — link first body / subassembly {"path": "/path/to/part.FCStd", "object": "X"} — link named object placement: [x, y, z] or {position: [...], axis: [...], angle_deg: ...}. mate: place by aligning this part's published interface frame to an already-placed parent's, instead of (or after) a raw placement: {"child_iface": "", "parent": "<link-name|handle>", "parent_iface": ""}. Frames come from publish_interface.
Inputs: source (required), assembly (required), mate, name, +2 more.
add_primitiveAdd PrimitiveA
Add a primitive to the active document.
kind: 'box' (uses w, d, h), 'cylinder' (uses r, h), or 'sphere' (uses r).
placement: optional [x, y, z] mm translation.
name: optional human-facing name. It sets the object's LABEL — what
list_objects, bom_extract and the drawing/manifest layers display — and
leaves the internal FreeCAD Name alone, since handles and register_handle
key off Name and it must stay unique and stable. Omit it and the label
stays the type default ('Box' / 'Cylinder' / 'Sphere'), which BOM and
designation checks read as an unnamed generic solid.
Returns {handle, name, label, volume}: name is FreeCAD's internal id and
label is the display name (equal to name when you passed none). The
handle (e.g. 'box_1') is how you reference this object in subsequent
boolean_op calls.
Inputs: kind (required), d, h, r, +4 more.
add_projection_groupAdd Projection GroupC
Add a multi-view projection group of body to a drawing page.
views: list of FreeCAD view codes ('Front', 'Top', 'Right', 'Left',
'Bottom', 'Rear', 'FrontTopLeft', etc.). Default: ['Front', 'Top', 'Right'].
Inputs: body (required), page (required), name, views, +1 more.
add_pulleyAdd PulleyA
Add a timing-belt (or V) pulley as a static solid. Axis is +Z; toothed belt face spans z in [0, width].
teeth: tooth count (>= 6). belt_pitch: belt tooth pitch mm/tooth (e.g. 2.0 for GT2, 3.0 for GT3/HTD-3M); pitch diameter PD = belt_pitch * teeth / pi. width: belt-face length mm. flanged: True adds two thin guide discs (radius PD/2 + 2*belt_pitch) at each end to retain the belt. height: optional mm; OVERRIDES width when given (default height = width). placement: optional [x, y, z] mm translation of the axis base. name: object label.
Returns {handle, name, volume, pitch_diameter, belt_pitch, teeth, width, flanged}. pitch_diameter (mm) is the mating number: the centre distance to a mating pulley plus the required belt length derive from the two pitch diameters and the same belt_pitch.
Inputs: teeth (required), width (required), belt_pitch (required), name, +4 more.
add_rackAdd RackA
Add a linear gear rack (a spur gear's straight counterpart) as a solid.
A rack is a gear of infinite radius: straight-flanked teeth on a rail. Standard full-depth tooth form (addendum = module, dedendum = 1.25module, tooth height = 2.25module, flanks at pressure_angle from vertical).
teeth: number of teeth (>= 1). module: mm (sets tooth size; circular pitch = module * pi). height: extrusion thickness mm along +Y (the rack's face width; default 6). width: mm, rail base-band thickness below the tooth root line (default 10). pressure_angle: deg, flank angle from vertical (default 20; 0 < pa < 45). placement: optional [x, y, z] mm translation of the rack origin. name: object label (default "Rack").
The profile lies in the XZ plane: root line at z=0, base band from z=-width to z=0, teeth from z=0 to z=2.25*module, extruded along +Y by height.
Returns {handle, name, volume (mm^3), pitch (mm/tooth = modulepi),
module, teeth, tooth_height (2.25module mm), length (teethmodulepi mm)}.
A spur gear MESHES with this rack when their pitch values match
(gear module*pi == rack pitch); length sizes the rail for the travel.
Inputs: teeth (required), module (required), name, width, +4 more.
add_ribAdd RibA
Add a reinforcing rib/web inside a PartDesign Body by thickening an OPEN sketch spine (line, arc, or connected polyline; NOT a closed loop) into a wall that fuses with the surrounding material. The sketch plane sets orientation.
thickness: wall thickness mm (> 0). midplane: centre the wall on the spine (default) or grow one side. reversed: flip if the rib lands on the wrong side of its sketch plane.
Returns {handle (starts 'rib_'), name, volume (the Body's mm^3 after — always greater), thickness}. Headless FreeCAD lacks PartDesign::Rib, so it is built as an equivalent midplane Pad of the offset spine; very intricate spines may differ. Raises ValueError on a closed/degenerate profile or thickness <= 0, RuntimeError if the rib adds nothing (spine does not span between walls).
Inputs: body (required), sketch (required), thickness (required), name, +3 more.
add_section_viewAdd Section ViewA
Add a cross-section view when the part has internal features the outline / hidden-line views convey ambiguously (counterbore, blind hole/bore, pocket). The need is judged from the real solid; the cut runs lengthwise through such a feature and the view is placed in clear space.
auto (default True): add it ONLY if hidden internal geometry exists, else return {added: False, recommended: False}; auto=False forces a section. process: 'auto' | 'prismatic' | 'turned' — how features are enumerated.
Returns {added, recommended, reasons, feature_ids, view?, normal?, origin?}.
drawing_gate's section_recommended tells you in advance.
Inputs: page (required), auto, name, symbol, +2 more.
add_sketch_constraintAdd Sketch ConstraintB
Add a constraint to a sketch.
type: Coincident, Horizontal, Vertical, Distance, DistanceX, DistanceY, Radius, Diameter, Equal, Parallel, Perpendicular, Tangent, Block, Symmetric, Angle. refs: list of [geom_idx, vertex_role] pairs. vertex_role: 0=edge, 1=start, 2=end, 3=center. value: numeric value (mm or radians) for dimensional constraints.
Inputs: refs (required), type (required), sketch (required), value, +1 more.
add_sketch_externalAdd Sketch ExternalA
Project an external edge/face/vertex into a sketch as construction geometry.
ref: {handle, edge: tag|'EdgeN'} (or face/vertex variant; or {handle, tag} where tag is e_/f_ prefixed). The projected element gets a negative geom index so subsequent constraints can reference it. Use this to make a sketch that stays anchored to upstream geometry (e.g. a hole 5mm from a tagged edge that survives pad-length edits).
Inputs: ref (required), sketch (required), workspace.
add_sketch_geometryAdd Sketch GeometryA
Append geometric primitives to a sketch.
items is a list of dicts:
{type: 'line', start: [x,y], end: [x,y], construction?: bool}
{type: 'circle', center: [x,y], radius: float}
{type: 'arc', center: [x,y], radius, start_angle, end_angle} (radians)
{type: 'point', pos: [x,y]} Returns {indices: [...]} — Sketcher-assigned indices for use in constraints.
Inputs: items (required), sketch (required), workspace.
add_springAdd SpringA
Add a helical compression spring: a round wire swept along a cylindrical helix.
All lengths in mm; angles n/a. wire_diameter: wire (stock) diameter d, mm. outer_diameter: spring outer diameter OD, mm (must be > wire_diameter). free_length: uncompressed overall length along the axis, mm. coils: number of turns (active coils), may be fractional. kind: 'compression' (only supported mode in v1; end coils are not squared yet). placement: optional [x, y, z] mm translation of the spring's base.
Geometry: mean coil diameter D = outer_diameter - wire_diameter; coil pitch = free_length / coils. Spring rate is computed for STEEL (shear modulus G = 79.3 GPa) as k = Gd^4 / (8D^3*coils), reported in N/mm.
Returns {handle, name, volume (mm^3), mean_diameter (mm), free_length (mm), coils, kind, solid_height (mm, = coils*wire_diameter, the fully-compressed block height), spring_rate_n_per_mm (N/mm)}. Use free_length, solid_height and spring_rate_n_per_mm to spec the spring into a mechanism (available travel = free_length - solid_height; force = spring_rate_n_per_mm * deflection).
Inputs: coils (required), free_length (required), wire_diameter (required), outer_diameter (required), +4 more.
add_sprocketAdd SprocketA
Add a roller-chain sprocket (ISO 606 / ANSI), built as a static solid plate.
teeth: tooth count (>= 3). chain_pitch: chain link pitch in mm (e.g. 12.7 for #40 / ANSI 40 chain). roller_diameter: chain roller diameter in mm. height: plate thickness in mm (default 6.0). placement: optional [x, y, z] mm translation.
Build: a disc of tip radius ~= pitch_radius + chain_pitch*0.3 with teeth
roller seats (circular pockets, radius roller_diameter/2 * 1.05) cut on the
pitch circle, one per tooth. This is a fit/visualisation approximation of the
true ISO 606 tooth form, not a load-rated profile.
Returns {handle, name, volume, pitch_diameter, chain_pitch, teeth, tip_radius,
bore}. pitch_diameter (mm) = chain_pitch / sin(pi/teeth) and chain_pitch are
the MATING numbers: a chain of the same chain_pitch wraps the sprocket, and the
centre distance between two sprockets derives from their pitch_diameters. bore
is 0 (no shaft hole cut yet — drill one with the hole command).
Inputs: teeth (required), chain_pitch (required), roller_diameter (required), name, +3 more.
add_threadAdd ThreadA
Generate a REAL helical ISO-style 60-degree thread as a static solid.
Unlike hole/list_thread_options (which only flag a thread as metadata),
this cuts actual helical geometry: a truncated triangular rib swept along a
helix and fused to a core cylinder.
All lengths in mm, angles in degrees. diameter: nominal MAJOR (crest) diameter, mm. For internal=True this is the bore the tap fits. pitch: thread pitch, mm per turn (e.g. M8 coarse = 1.25). length: threaded length along +Z from z=0, mm. internal: False (default) -> a finished externally-threaded stud. True -> a TAP/insert cutting-tool solid sized to the bore; fuse it into (or cut it from) a bored hole in your part to produce a threaded bore. starts: number of thread starts, >=1. Multi-start repeats the helix rotated by 360/starts and uses lead = pitch*starts. grade: optional material / property class as ordered ("8.8", "A2"). Changes no geometry; it completes the DIN 976-1 threaded-rod designation stamped on an EXTERNAL single-start thread — that solid is studding, a thing you buy by the metre. An internal thread is a tap-shaped cutting tool and a multi-start is not a stock item, so neither is designated at all. placement: optional [x, y, z] mm translation of the solid's base (default at the origin, axis along +Z). name: optional object name.
Geometry note: the modeled minor (root) uses the ISO 5H/8 truncation; the reported minor_diameter uses the standard ISO formula diameter - 1.0825*pitch. Fallback behaviour: if the helical sweep cannot produce a valid solid the tool returns a plain cylinder tagged with the thread spec and modeled=False (this is rare for sane M-series inputs); always check the modeled flag.
Returns {handle, name, volume (mm^3), major_diameter (mm), minor_diameter
(mm), pitch (mm), length (mm), starts (int), internal (bool), modeled (bool),
designation, orderable, catalog}. Studding is bought by the bar and cut, so any
length up to the longest stock bar reads as stocked with a note that it is a
cut.
Mating numbers: drill/bore minor_diameter to tap an internal thread; clear a
major_diameter (+clearance) hole to pass an external stud.
Inputs: pitch (required), length (required), diameter (required), name, +5 more.
add_thumbnailAdd ThumbnailA
Place a small isometric pictorial (vector TechDraw view) in the sheet's top-right corner IF it fits without crowding existing views/dimensions. It is pinned there, never dimensioned, and ignored by drawing_gate. Call it AFTER views, dimensions and fit_page.
Returns {placed, box, scale?, view?, reason?}; placed=False with a reason when the corner is occupied.
Inputs: page (required), name, workspace.
annotate_faceAnnotate FaceADestructive
Declare the semantic ROLE of a face — what it is FOR — so later edits can be checked against intent instead of re-derived from raw geometry. The role binds to the face's stable f_* tag and persists in the .FCStd as a JSON property bag (same mechanism as publish_interface); it survives save/reopen. Once declared, check_airtight_path accepts the role/name directly (e.g. inlet="inlet").
handle: the part. face: an f_* tag, 'FaceN', or int index of the face to annotate. role: one of 'inlet' | 'outlet' | 'sealing' | 'wetted' | 'ambient' | 'mating'. name: optional unique label for this annotation (default: the role, then role_2, role_3, …); re-using a name updates that annotation. meta: optional dict stored verbatim (e.g. {"spec": "32mm hose"}).
Returns a dict: {handle, name (the annotation key used), role, tag (the f_* the role is bound to), index ('FaceN' at annotation time), roles (sorted list of all annotation names now on the part)}.
Inputs: face (required), role (required), handle (required), meta, +2 more.
assembly_lockAssembly LockADestructive
Write a lockfile recording each component's content hash, published- interface hash, and mate dependencies — the provenance baseline a coordinator uses to detect drift across a team. Call after a clean merge. lockfile defaults to .lock.json. Returns {lockfile, components}.
Inputs: manifest (required), lockfile, workspace.
assembly_lock_checkAssembly Lock CheckARead-only
Compare current component files to a lockfile and classify drift (change propagation, RFC §9): modified — file changed since lock interface_changed — published interface frames moved (subset of modified) stale — mates to an interface_changed component and was NOT itself rebuilt: a neighbor that needs re-dispatch new / removed — components added to / dropped from the manifest ok = nothing stale and no new/removed (safe to re-merge without re-dispatch). Returns {modified, interface_changed, stale, new, removed, ok}.
Inputs: manifest (required), lockfile, workspace.
async_demo_submitAsync Demo JobA
Reference async long-solve: launch a job that runs OFF the MCP channel and return immediately, so a multi-minute solve never blocks the worker. (This demo just computes for duration_s then returns a deterministic result; a real FEM/CFD solve plugs into the same facility — see ankusdrive/jobs.py.) Returns {job_id, status, cache_hit}; poll with job_status / job_result. A re-submit with identical (duration_s, value) is a content-hash cache hit (no recompute).
Inputs: value, workspace, duration_s.
balloon_drawingBalloon DrawingADestructive
Number every characteristic on a drawing page (dimensions, feature control frames, feature notes) with an inspection balloon, rendered on SVG/PDF export; inspection_plan and fai_report key their rows on it.
Numbers are a persistent IDENTITY: re-running reassigns nothing, a new dimension gets the next number, a deleted one RETIRES its number (never reused). renumber=True restarts from 1 and invalidates existing inspection records. Call after dimensions are placed and fit_page has run.
Returns {count, balloons (source object name -> number), assigned, kept, retired, next_balloon}.
Inputs: page (required), renumber, workspace.
bar_impactBar ImpactARead-only
St-Venant bar impact, exact (NO solver): a uniform bar striking a rigid wall
end-on. Face stress σ = ρ·c₀·v₀ with c₀ = √(E/ρ) — mass, area and length cancel,
so only a slower strike or a softer/lighter material lowers it; contact lasts
2L/c₀ and the bar leaves stress-free at the strike speed (restitution 1). Past
the yield velocity v_y = σ_y/(ρ·c₀) the face yields and a bilinear material
(tangent_mpa) caps at σ_y + ρ·c_p·(v₀ − v_y), c_p = √(E_t/ρ). E and ρ from
youngs_gpa/density_kg_m3 or a Materials-DB material (which also supplies
yield). area_mm2 adds force_n. The closed-form twin impact_dynamics_submit is
gated against; also the quick ceiling on what any strike at this speed can do to
this material. Uniaxial-stress (slender bar) theory.
Returns {wave_speed_m_s, stress_mpa, elastic_stress_mpa, force_n, contact_duration_ms, rebound_velocity_m_s, yield_velocity_m_s, plastic, plastic_wave_speed_m_s, fidelity, band_pct, valid_range_ok, warnings, escalate_to}.
Inputs: length_mm (required), velocity_m_s (required), area_mm2, material, +5 more.
baseline_createBaseline CreateBDestructive
Pin a labeled, immutable BASELINE (issue #142, C3) — a {item: revision + content fingerprint} snapshot over an items.json registry (a git-tag / lockfile over the item graph) for reproducible rebuilds. The fingerprint pins the artifact BYTES, so a rebuild is verifiable byte-for-byte.
label: the baseline label (e.g. "v1.0"). registry: path to the items.json sidecar. items: optional subset of item ids to pin (default: every item). base_dir: artifact root for fingerprinting (defaults to the registry's directory). note: optional description. out: optional path — write the git-diffable baseline sidecar there.
Returns the baseline object. Deterministic: same state -> identical bytes.
Inputs: label (required), registry (required), out, note, +3 more.
baseline_verifyBaseline VerifyARead-only
Verify a rebuild against a pinned baseline (issue #142, C3) — the reproducible- rebuild gate. Re-resolves every pinned item from the current registry + artifacts and checks it still matches the pinned rev AND content fingerprint; a drifted input (changed bytes, a bumped rev, a vanished item) is caught, never silently accepted.
baseline: path to the baseline sidecar. registry: path to the current items.json sidecar. base_dir: artifact root for fingerprinting (defaults to the registry's directory).
Returns {ok, label, drifted (item/field/expected/actual), missing} — ok is True iff the rebuild reproduces the baseline exactly.
Inputs: baseline (required), registry (required), base_dir, workspace.
beam_bucklingBeam BucklingARead-only
Exact column-buckling screen (Euler + Johnson, NO solver) — the closed-form
twin the CalculiX fem_buckling eigen-solve is gated against (as beam_modal
is to fem_modal). Section: width_mm+height_mm (solid rectangle, weak axis
automatic), diameter_mm (round), or explicit area_mm2+i_min_mm4. E/σ_y from
youngs_gpa/yield_mpa or a Materials-DB material. end_condition:
'pinned_pinned' | 'fixed_free' | 'fixed_pinned' | 'fixed_fixed' (theoretical K).
Euler σ_cr=π²E/λ² above the transition slenderness √(2π²E/σ_y), Johnson
parabola below (both exactly σ_y/2 at it). With load_n the safety factor
P_cr/P is returned. Escalate to fem_buckling for non-prismatic /
eccentric / built-up cases.
Returns {end_condition, k_factor, slenderness, transition_slenderness, governing, sigma_cr_mpa, p_cr_n, area_mm2, i_min_mm4, radius_gyration_mm, safety_factor, fidelity, band_pct, valid_range_ok, warnings, escalate_to}.
Inputs: length_mm (required), load_n, area_mm2, material, +8 more.
beam_modalBeam ModalARead-only
Exact Euler-Bernoulli natural frequencies of a uniform rectangular beam — the
closed-form modal oracle (no solver), and the band the CalculiX fem_modal
eigen-solve is gated against. f_n = (βL)_n²/(2π)·sqrt(E·I/(ρ·A·L⁴)); the beam bends
in height_mm (I = width·height³/12, so a slender beam's lowest mode is the
thinnest-direction bend). boundary: 'cantilever' | 'simply_supported' |
'clamped_clamped' | 'free_free' | 'clamped_pinned' (up to 5 modes each). Material
via youngs_gpa+density_kg_m3, or a Materials-DB material name. Slender-beam
theory — accurate while length ≫ height (thick beams need a Timoshenko correction).
Returns {boundary, n_modes, frequencies_hz, beta_l, first_mode_hz, youngs_gpa, density_kg_m3, area_mm2, I_mm4, slenderness}.
Inputs: width_mm (required), height_mm (required), length_mm (required), n_modes, +5 more.
bearing_lifeBearing LifeARead-only
Basic rating life L10 (ISO 281): L10=(C/P)^p rev (p=3 ball, 10/3 roller), L10h=L10*1e6/(60n). Supply the dynamic rating C as dynamic_load_c_n, or pull it from the deep-groove ball catalog by designation (e.g. "6205" -> C=14.0 kN, also reporting bore/OD/width, C0 and static safety factor). Returns {l10_million_rev, l10_hours, load_ratio, dynamic_load_c_n, exponent, pass, ...} (pass vs target_hours when given).
Inputs: speed_rpm (required), equivalent_load_p_n (required), kind, workspace, +3 more.
belt_driveBelt DriveBRead-only
Rate a belt drive (Eytelwein/capstan). Wrap theta=pi-2asin((D-d)/2C), Fe=P/V, T1/T2=e^(mu*theta) (V-belt divides mu by sin(beta/2)). Returns {wrap_angle_deg, belt_speed_m_s, effective_force_n, tension_ratio, tight_side_n, slack_side_n, transmissible_power_w, pass}.
Inputs: power_w (required), small_pulley_rpm (required), center_distance_mm (required), large_pulley_dia_mm (required), +5 more.
bolted_joint_checkBolted Joint CheckARead-only
Rate a bolted joint (VDI 2230-lite). Geometry from bolt_dia_mm (+pitch_mm) OR
a standard thread named via bolt_size ("M8"/"M8x1.0") that pulls dia/pitch and
the standards-table tensile stress area; proof strength from the ISO 898-1
property_class ("8.8") when given. Preload from torque via T=KFd (pass
torque_nm OR preload_n). Returns {preload_n, tensile_stress_area_mm2,
bolt_stress_mpa, preload_pct_proof, bolt_stress_with_load_mpa,
separation_load_n, separation_margin, pass, governing}. preload_target_pct is
the fraction of proof strength the preload is judged against (0.75 default;
0.65 for a reused bolt, 0.90 for a critical permanent joint).
Inputs: k_factor, material, pitch_mm, bolt_size, +9 more.
bom_extractBOM ExtractARead-only
Walk an assembly and return [{part, count, total_volume_mm3, total_mass_kg?}, ...] grouped by source (component file + object), NOT the bare object name, so two distinct components both named "Box" don't collapse into one row. density (kg/mm³) is optional.
recursive (default True): descend into linked subassemblies (App::Part) so the BOM flattens to leaf parts. False counts each subassembly as one line.
orderable (default False): opt in to the BUYABILITY view — is every purchased line on this BOM a part that actually exists off the shelf? The default return is unchanged — a bare list — because everything downstream consumes it. With orderable=True you instead get a dict:
rows the same BOM rows, each also carrying designation / standard / part_class plus the catalog verdict (stocked, catalog_code, catalog) consumables purchased parts that are NOT modelled objects and would otherwise never reach a BOM — today the O-ring an oring_groove was cut for, counted across every part that calls for it undesignated purchased rows a buyer cannot order from (no designation) not_stocked rows naming a part nobody stocks, each with a reason and the nearest stocked alternatives designation the full designation_check verdict stocked_count how many purchased lines resolved to a stocked item ok False when anything purchased is undesignated OR not stocked
check_stock=False designates without checking availability. A design built out of
fasteners that do not exist is the failure this catches, and the offending rows
stay IN the list rather than being quietly dropped. Availability is a curated
snapshot of a market (captured, market), not physics.
Inputs: assembly (required), density, orderable, recursive, +2 more. Returns: result
boolean_opBoolean OpA
Boolean operation on two objects by handle.
op: 'cut' (base minus tool), 'fuse' (union), or 'common' (intersection). strict: raise instead of warning on a degenerate cut.
Returns {handle, volume, removed_volume (base - result mm3; negative on a
fuse), volume_ratio (result/base, None if base empty)}, plus warnings ONLY
on a degenerate cut ("warnings" in result is the test): annihilation (result
~0 — later features operate on nothing) or miss (result == base — nothing
removed). Warn-don't-fail by default; strict=True makes both errors.
Inputs: op (required), base (required), tool (required), strict, +1 more.
bounding_boxBounding BoxARead-only
Axis-aligned bounding box (AABB) of a shaped object, mm, world coordinates. A measurement: returns numbers, creates nothing.
QUIRK (#284): min/max/size are OCC's ANALYTIC box, an UPPER bound. A trimmed
face is boxed by its untrimmed carrier surface, so cuts through fillets,
chamfers, lofts or spheres can report mm of material that isn't there (a real
trim at X=-32.0 read -36.7). Before calling a part wrong-sized, check
verified and pass tight=True.
oriented: also return the minimum-volume box at any orientation (OBB) under "oriented" (null if unsupported). Same analytic over-estimate. tight: also tessellate and return the mesh-derived box under "tight" — the trustworthy numbers. Opt-in: ~1.4s on a 200mm plate with 60 filleted holes. deflection: mesh chord tolerance mm for tight (default diagonal/2000, floor 0.001).
Returns {min, max, size, center ([x,y,z] mm, analytic), diagonal (mm), oriented (null | {size, center, diagonal}), verified, tight (null | {min, max, size, center, diagonal, deflection, triangles}), warnings (which face over-estimates, by how many mm)}. verified: "exact" (vertices touch all six faces) | "mesh_agrees" (tight found no disagreement) | "unverified" (usual on curved parts: upper bound only, re-run tight=True) | "over_estimate" (use "tight"; min/max/size wrong-big). The true box lies between "tight" and the analytic box.
Inputs: handle (required), tight, oriented, workspace, +1 more.
catalog_checkCatalog CheckARead-only
Is this exact part something you can buy off the shelf?
Pass a canonical designation ("ISO 4762 M4×12 A2", "608-2RS") or the handle of
a part whose designation was stamped when it was generated. Offline and
deterministic.
Returns {ok, code, standard, size, length, stocked, grade_ok, reason, nearest,
lengths, grades, fidelity, captured, market}, where code is:
stocked the size/length exists and the material is listed (for a
cut-to-length product like threaded rod, any length up to the
longest stock bar counts, with a note that it is a cut)
not_stocked the size exists but the LENGTH is not a stocked rung —
nearest names the rungs either side
size_not_stocked the standard does not cover this size at all
grade_not_listed the size exists, that material does not
not_catalogued the product standard is outside this corpus's coverage. That
is an absence of evidence, explicitly NOT a claim that the
part is unavailable — check not_covered from catalog_search
undesignated there was no designation to check
ok is True only for stocked.
Inputs: handle, workspace, designation.
catalog_nearestCatalog NearestARead-only
Snap a desired standard part to the nearest one that actually exists.
This is the call that changes how you design. Ask for an ISO 4762 M4×13 and it tells you that 12 and 16 are stocked and 13 is not — which turns "I need a 13 mm screw" into "I need to adjust my stack-up to 12 or 16". Use it the moment a fastener length falls out of a dimension chain, before the geometry hardens around a size nobody sells.
standard: a product standard or alias ("ISO 4762", "DIN 912", "ISO 7380-1"). size: thread designation, nominal mm, or shaft/bore mm. length: the wanted length in mm. Omit it for a product with no length dimension (a nut, a washer, a circlip), or to list the whole stocked ladder. grade: optional property class / material, used to complete the designation the call hands back.
Exact arithmetic on a DISCRETE ladder: nothing is interpolated, and nothing is silently rounded on your behalf. A length between two rungs is not a part, so you get both rungs and the signed deltas and you decide which way to move.
Returns {ok, standard, name, size, requested_length, exact, stocked, below, above,
nearest [{length, delta}], lengths, grades, designation, reason, fidelity,
captured, market}. designation is the canonical designation of the RECOMMENDED
part, so the answer is directly usable in a BOM. ok=True means what you asked for
is already stocked.
Inputs: size (required), standard (required), grade, length, +1 more.
catalog_searchCatalog SearchARead-only
Browse the off-the-shelf catalog: which standard components actually EXIST, in which sizes, in which stocked lengths. Call it while designing, before you commit geometry to a number — it is the difference between a design somebody can build and one that needs a special.
The dimensional corpora (threads/bearings/stock) tell you what a part measures. This one tells you whether it is a thing you can buy. Nothing else in AnkusDrive knows that an ISO 4762 M4×12 is a stocked item and an M4×13 is not.
Every argument is an optional filter: family screw | set_screw | nut | washer | retaining_ring | pin | bearing | oring | threaded_rod standard a product standard or alias — "ISO 4762" or "DIN 912" kind the product's kind tag (socket_head_cap_screw, nyloc_nut, ...) size thread designation, nominal mm, shaft/bore mm, or AS568 dash length an exact stocked length, or a min_length/max_length window (mm) grade property class / material ("8.8", "A2-70", "NBR70") drive hex_socket | hex limit max rows (default 50)
Offline, deterministic, zero network.
Returns {ok, count, truncated, items, standards, fidelity, captured, market,
not_covered}. Each item is {standard, name, family, kind, drive, size, size_kind,
lengths (the stocked ladder, narrowed to any length filter), length_count,
grades, length_measured, designation?}. Availability is a curated snapshot of a
MARKET (see market and captured), not physics — and read not_covered before
concluding from an empty result that a part does not exist.
Inputs: kind, size, drive, grade, +7 more.
cfd_body_dragCFD Body DragARead-only
Analytic EXTERNAL-flow drag screen (NO solver, milliseconds) — the external twin of cfd_pipe_flow, and the banded oracle the wind-tunnel solve cfd_external_flow_submit(model=…) is checked against. Use this FIRST to narrow a design space; escalate to the solve only for the shapes that survive.
Three families:
shape='sphere'— Clift–Gauvin over the whole standard drag curve, Cd = 24/Re·(1+0.15·Re^0.687) + 0.42/(1+4.25e4·Re^-1.16). Collapses to the EXACT Stokes 24/Re as Re→0; valid to Re=2e5 (it does not model the drag crisis). Passdiameter_mm+velocity_m_s.shape='cylinder'— Sucker–Brauer crossflow Cd (axis ⟂ flow), Cd ≈ 10 at Re=1, 1.45 at Re=100, 1.2 at Re=1e5. Passdiameter_mm,velocity_m_s, optionallength_mm(default 1 m, i.e. drag per unit span; L/D<10 warns about end relief).a tabulated bluff/streamlined shape — 'cube_face_on', 'flat_plate_normal', 'hemisphere_open_back', 'streamlined_body', 'car_modern', … (
shape='list'returns the whole table). Needsfrontal_area_mm2(or amodelhandle, whose silhouette alongflow_directionis measured off the live solid) +velocity_m_s;cdoverrides the table with a known value.
Fidelity: sphere/cylinder are correlations with band_pct 10/15; the table is band_pct 20 and only valid for Re ≈ 1e4–1e6 on a shape that genuinely matches.
Returns {cd, drag_force_n, frontal_area_m2, dynamic_pressure_pa, velocity_m_s, fidelity, band_pct, escalate_to} plus {reynolds, regime, valid_range_ok, warnings} for sphere/cylinder — or {shapes: {name: cd}} for shape='list'.
Inputs: cd, fluid, model, shape, +9 more.
cfd_external_flow_submitCFD External Flow SubmitADestructive
External-flow CFD (drag/lift) via OpenFOAM or SU2, asynchronous. Requires an
OpenFOAM (apt/conda) or SU2 binary; when none resolves this returns {ok:false,
reason, install} rather than raising. The two case-BUILDING modes below need
OpenFOAM specifically — they emit OpenFOAM dictionaries; SU2 only ever runs a
case_dir you prepared yourself, which is why solve_capabilities does not count
it toward the cfd family's any_available (issue #237). Three modes:
Put a real solid in the virtual wind tunnel (issue #223): pass a
model(orbody) handle +velocity_m_s. The solid's faces tessellate into an STL, a farfield box is auto-sized around it by standard practice (5L upstream / 10L downstream / 5L lateral, overridable viaupstream_factor/downstream_factor/lateral_factor; the reportedblockage_ratiowarns past 5 %), snappyHexMesh carves the body out, and theforcesfunction object integrates pressure + viscous traction over it. Cd/Cl/Cm come back on the MEASURED frontal silhouette alongflow_direction(default +x; exact for a convex body — override withfrontal_area_mm2for a re-entrant one) andreference_length_mm(default: the largest bbox dimension). Mesh knobs:base_cell_mm(default L/2 — a coarser cell is REJECTED, since snappy would then mesh an empty tunnel and report ~0 drag),surface_refine[min,max] levels,wake_refine,stl_tolerance_mm,end_time. Trust: the laminar path is gated live against the sphere drag curve at Re=1 and Re=100 (within ~2 %);turbulence='kOmegaSST'runs but has no verified oracle for arbitrary bodies and comes backgated:false. Past Re≈1000 a laminar request is flagged inwarningsrather than silently answered.Build the flat-plate validation case (no
model): passvelocity_m_s, with optionalplate_length_mm(default 100), afluidname ('air-20c','water-20c',…) or explicitmu_pa_s+rho_kg_m3, and mesh knobsnx_plate/n_y/end_time. THIS MODE SOLVES A FLAT PLATE, never the caller's geometry: a 2-D laminar plate with a clean leading edge (slip→plate→slip, far-field top), whose wall-shear drag is integrated from the converged U field and returned next to the Blasius reference Cf=1.328/√Re_L (blasius_ratio≈1, ~15 %).turbulence='kOmegaSST'upgrades it to RANS (default plate_length 1000 mm so Re_L > transition), gated BANDED against the mixed-transition Cf = 0.074·Re^(−1/5) − A/Re.Run a prepared
case_dir(optionally anapplication); OpenFOAM runs with its environment sourced, an SU2 case (*.cfg+*.su2mesh) runs as a direct native subprocess — no bash/WSL needed, including on Windows.
Returns the degradation dict, or {job_id, status, cache_hit}; poll job_result. Body mode: {ok, returncode, cd, cl, cm, drag_force_n, drag_pressure_n, drag_viscous_n, lift_force_n, force_total_n, moment_total_nm, force_drift_pct, n_force_samples, reynolds, reference_length_m, frontal_area_m2, frontal_area_source, moment_reference_m (the bbox centre moments are taken about, not the global origin), blockage_ratio, base_cell_m, converged, gated, warnings, case_dir}. Flat plate: {ok, returncode, reynolds_l, cd, cf_solved, cf_blasius, blasius_ratio, drag_force_n, drag_momentum_n, drag_blasius_n, n_cells, case_dir}; RANS plate swaps the gate fields for {cf_solved (momentum), cf_mixed_ref, cf_mixed_ratio, cf_turbulent_ref, cf_wall_corrected, y_plus_estimate, band_pct}. Prepared case: {ok, returncode, solver, application, case_dir, kind, stdout_tail}.
Inputs: n_y, body, fluid, model, +20 more.
cfd_internal_flow_submitCFD Internal Flow SubmitADestructive
Internal-flow CFD (pressure drop) via OpenFOAM or SU2, asynchronous. Requires an
OpenFOAM (apt/conda) or SU2 binary; when none resolves this returns {ok:false,
reason, install} rather than raising. The pipe and geometry-bridge modes need
OpenFOAM specifically — they emit OpenFOAM dictionaries — but channel_height_mm
builds a NATIVE SU2 case, which is why solve_capabilities counts SU2 toward the
cfd family (issue #237). On Apple Silicon that is the difference between needing
a Multipass VM and not. turbulence='kOmegaSST' upgrades the pipe
validation case to RANS (SIMULATION_NEXT B3): wall-function k/ω/ν_t with first-cell
y+ targeted at ~30–100, developed dp/dx fitted over the second half of a ≥40·D pipe,
gated BANDED against Colebrook (colebrook_ratio ≈ 1 ± 10 % — the Moody correlation
is itself a band, never an exact gate). Four modes:
Build the native SU2 plane-channel case (no OpenFOAM, no VM): pass
channel_height_mm, optionallychannel_length_mm(default 10× the height),velocity_m_s(default Re 50), afluidormu_pa_s+rho_kg_m3(default a light oil — holding Re low with water means millimetres per second, where SU2's incompressible pseudo-time is badly scaled),nx/ny,max_iterations. Gated against the EXACT plane-Poiseuille closed form Δp = 12·μ·U·L/h², returningpoiseuille_ratio≈ 1. The inlet is the fully developed parabolic profile, so there is no entrance-length error to drown out with a long domain.Build the straight-pipe validation case (no case prep): pass
diameter_mm,length_mm, andvelocity_m_s(orflow_rate_lpm), with afluidname or explicitmu_pa_s+rho_kg_m3(mesh density vian_axial/n_radial, iterations viaend_time). The handler builds the axisymmetric laminar pipe, runs blockMesh+simpleFoam, and returns the solved Δp next to the Hagen–Poiseuille analytic reference (cfd_pipe_flow) — the kickoff's exact CFD gate,hp_ratio≈1.Solve a real FreeCAD solid — the geometry bridge: pass a
bodyhandle plusinlet_face/outlet_face(1-based indices into the solid's faces; every other face becomes a no-slip wall) andvelocity_m_s(applied along the inlet face's inward normal). The solid tessellates into a multi-region STL and meshes with blockMesh + snappyHexMesh;base_cell_mmsets the background cell size,location_in_mesh_mmthe kept-region seed point (default: bbox centre — set it for non-convex solids),stl_tolerance_mmthe tessellation sag. Use the developed-profilepressure_drop_pa; also passdiameter_mm+length_mmto get anhp_ratioreference for pipe-like bodies.Run a prepared
case_dir(optionally anapplication, e.g. 'simpleFoam'/'foamRun'); for OpenFOAM its environment is sourced before the run, while an SU2 case (*.cfg+*.su2mesh) runs as a direct native subprocess — no bash/WSL needed, including on Windows.
Returns the degradation dict, or {job_id, status, cache_hit}; poll job_result. Pipe/body cases: {ok, returncode, pressure_drop_pa (developed), pressure_drop_inlet_pa, hagen_poiseuille_pa?, hp_ratio?, n_cells, case_dir}; RANS pipe adds {dpdx_pa_m, dpdx_colebrook_pa_m, colebrook_ratio, y_plus_estimate, band_pct}. Prepared case: {ok, returncode, solver, application, case_dir, kind, stdout_tail}.
Inputs: body, fluid, mu_pa_s, n_axial, +19 more.
cfd_mesh_independence_submitCFD Mesh Independence SubmitA
Solve the same CFD case at 2-3 refined meshes and report the Grid Convergence Index — asynchronous, one job for the whole ladder. Requires OpenFOAM; degrades to {ok:false, reason, install} when it does not resolve.
Answers the question no single solve can: is this number a property of the flow, or of the mesh? On geometry with no analytic twin that band is the only error bar available, and it is what turns "a solver produced 0.31" into "0.31 ± 2 %".
Two families, dispatched like their single-solve twins:
the wind tunnel — pass
model(orbody) +velocity_m_s, plus any cfd_external_flow_submit knob. The ladder varies the background cell; the body is tessellated ONCE and shared, so the study isolates mesh error instead of mixing in a changing STL. Defaultmetric'cd'.the straight pipe — pass
diameter_mm,length_mm,velocity_m_s(orflow_rate_lpm). The ladder scalesn_axial/n_radial. Defaultmetric'pressure_drop_pa'.
The COARSEST level is the mesh a plain submit would have built and the study refines
from there (the tunnel's default cell is already the coarsest that resolves the body
at all). Cost therefore grows as the cube of refinement_ratio: 3 levels at 1.5 puts
roughly 11x the cells in the finest mesh, so budget accordingly. end_time is the
cap for the COARSEST level and is scaled up for the finer ones — a fine mesh needs
proportionally more sweeps, and a study whose finest level quietly stopped at its cap
is worthless.
Returns the degradation dict, or {job_id, status, cache_hit}; poll job_result for {ok, family, metric, levels: [{label, value, cell_size_m, n_cells, converged, returncode, case_dir}], grid_convergence: {observed_order, extrapolated_value, gci_pct, monotonic, asymptotic_ratio, order_clamped, warnings, ...}, band_pct, extrapolated_value, hagen_poiseuille_pa (pipe family), warnings}.
Inputs: body, fluid, model, levels, +16 more.
cfd_pipe_flowCFD Pipe FlowARead-only
Analytic straight-pipe pressure drop (NO solver) — the fast internal-flow screen
and the exact gate the OpenFOAM cfd_internal_flow solve is checked against. Laminar
(Re<2300) is Hagen–Poiseuille Δp = 128·μ·L·Q/(π·D⁴) with its D⁴ scaling — exact;
turbulent uses smooth-pipe Blasius, or Colebrook–White when roughness_mm is given
(the Colebrook value is always reported for turbulent flow) — a ±10 % Moody-band
correlation (fidelity labeled). Give flow as flow_rate_lpm or velocity_m_s;
fluid μ,ρ from a name ('water-20c','air-20c','oil-sae30-20c','glycerin-20c') or
explicit mu_pa_s+rho_kg_m3. Escalate turbulent cases to
cfd_internal_flow_submit(turbulence='kOmegaSST').
Returns {reynolds, regime, velocity_m_s, flow_rate_m3_s, friction_factor, colebrook_friction_factor, relative_roughness, pressure_drop_pa, wall_shear_pa, hagen_poiseuille_pa, laminar, fidelity, band_pct, escalate_to}.
Inputs: length_mm (required), diameter_mm (required), fluid, mu_pa_s, +5 more.
chain_driveChain DriveARead-only
Rate an ANSI roller-chain drive (ASME B29.1). Rated at the lower of the
link-plate-fatigue (HP1=0.004N1^1.08n1^0.9P^(3-0.07P), low speed) and
roller-impact (HP2=1000KrN1^1.5P^0.8/n1^1.5, high speed) envelopes, P in
inches. Give chain_pitch_mm (matching add_sprocket) OR a chain_number
("40","60",...) for pitch+Kr; strands scale by the B29.1 factor. Powers in W.
Returns {rated_power_w, type1_power_w, type2_power_w, governing, strands,
strand_factor, ..., power_sf?, pass}. k_r overrides the roller-impact
constant the chain_number lookup supplies (29 for the 25-240 series, 17 for
the lightweight #41) — needed for a chain outside the ANSI table.
Inputs: speed_rpm (required), teeth_small (required), k_r, power_w, +4 more.
chamfer_edgesChamfer EdgesA
Chamfer (bevel) edges of a shaped Part object — fillet_edges' counterpart.
edges: e_* tags (preferred), 'EdgeN' strings, or 1-based ints. size: symmetric leg distance mm (> 0). per_edge / allow_partial: as fillet_edges. The base object is hidden. Validated exactly like fillet_edges (#283).
Returns {handle, name, volume (mm^3), edges (indices chamfered), checks {valid, solids, envelope_ok, envelope_growth_mm, envelope_tol_mm}, mode, partial}; when partial, also skipped_edges and a warning. On a failed check the feature is removed and BlendCheckFailed names the offending edges.
Inputs: edges (required), handle (required), name, size, +3 more.
change_impactChange ImpactARead-only
The where-used impact report for a set of changed items over a lockfile graph
(issue #142, C3) — surfaces the §9 stale set as an item-level impact report
BEFORE a change is committed.
lockfile: path to the lockfile (the §9 dependency graph). changed: the list of changed item / component ids (an ECO's affected set).
Returns {changed, stale (the §9 immediate re-dispatch consumers), where_used (the full transitive blast radius), ok (true iff nothing is impacted)}.
Inputs: changed (required), lockfile (required), workspace.
check_airtight_pathCheck Airtight PathARead-only
Functional check for an enclosed-flow part (a vacuum adapter, manifold,
duct): is there a single connected void joining the inlet to the outlet,
bounded by solid everywhere else? This catches what check_shape cannot — a
watertight solid can still have a blocked flow path or a hidden leak.
Inspection only: measures, returns no handle, mutates nothing.
handle: the part to inspect. inlet / outlet: a face reference naming each port OPENING (the rim face around the hole) — an f_* tag, 'FaceN', int index, or a role/name declared with annotate_face (e.g. "inlet"). Both ports are sealed with cap solids and the negative-space void is analysed. min_aperture_mm2: optional minimum acceptable bottleneck cross-section; a connected-but-pinched path (a near-zero 'almond slit') then fails. pad_mm: optional bounding-box margin (default max(2.0, 0.05*diagonal)).
Returns a dict (lengths mm, areas mm², volumes mm³): ok (bool) connected AND not leaky AND aperture >= threshold status (str) 'airtight' | 'bottleneck' | 'blocked' | 'leaky' connected (bool) one void joins inlet and outlet leaky (bool) with both ports capped the cavity still reaches ambient, so an unintended opening exists min_aperture_mm2 (float|null) narrowest section of the flow void bottleneck_point ([x,y,z]|null) a point on the narrowest section plane flow_void_volume_mm3 (float|null) volume of the connecting void void_components (int) number of void solids (ambient + enclosed) inlet / outlet (str) the resolved 'FaceN' references pad_mm (float) the margin used
Inputs: inlet (required), handle (required), outlet (required), pad_mm, +2 more.
check_shapeCheck ShapeARead-only
Check a shaped object's validity and topology before building on it — a guard after booleans/sweeps/imports. Inspection only; does NOT auto-repair. Watertight does not mean an internal channel is unobstructed: for ducts and manifolds use check_airtight_path.
Returns {valid (OCC topology/geometry sound), watertight_solid (one solid AND valid AND closed — the 'safe to keep building' verdict), shape_type ('Solid', 'Shell', 'Compound', ...), closed (no free edges), solids (want 1), shells, faces, edges, volume_mm3 (0 for open/2D), is_null, check (only when invalid; diagnostics in the worker log), check_error (only if diagnostics raised)}.
Inputs: handle (required), workspace.
cht_channel_submitCHT Channel SubmitADestructive
Conjugate heat transfer via Elmer (P3 M6 frontier), asynchronous — ONE solve spanning a plug-flow fluid channel AND a conducting solid wall coupled at their shared interface. Requires ElmerSolver; when absent this returns {ok:false, reason, install} rather than raising.
Builds the two-body channel (constant outer flux_w_m2, inlet Dirichlet
t_in_c, all else adiabatic) whose gates are exact WITHOUT a Nusselt
correlation: the outlet bulk temperature follows the energy balance
q″·L = ṁ·c_p·ΔT and the solid-layer drop is q″·t/k. Defaults are the
live-validated water channel (cell Péclet ≈ 9 — the builder rejects > 25, where
stabilized advection visibly leaks the energy balance). Also accepts a prepared
case_dir.
Returns the degradation dict or {job_id, status, cache_hit}; poll job_result for {ok, t_outlet_mean_c, t_out_exact_c, energy_balance_ratio (≈1, ±3%), dt_solid_k, dt_solid_exact_k, solid_drop_ratio (≈1), pe_cell, case_dir}.
Inputs: nx, sif, t_in_c, k_fluid, +12 more.
cht_graetz_submitCHT Graetz SubmitADestructive
Flow-coupled Graetz channel via Elmer (SIMULATION_NEXT B4), asynchronous — the TRUE Nusselt validation that upgrades cht_channel_submit's plug flow: FlowSolve computes the real laminar profile and HeatSolver rides on it (Convection = Computed) between two isothermal walls. Requires ElmerSolver; when absent this returns {ok:false, reason, install} rather than raising.
Two gates: the solved parabola's u_max/u_mean ≡ 3/2 exactly, and the developed
mixing-cup decay d ln(T_wall−T_bulk)/dx fitted over the second half of the
channel yields Nu, gated against the Graetz eigenvalue Nu_T = 7.5407 (parallel
plates, constant wall temperature) — a slug profile would give π² = 9.87, so
the gate also proves the profile coupling is real. This closes the loop with
the h_estimate correlation screen. The writer polices Re < 400, development
lengths inside the first 45 %, and cell Péclet ≤ 25. Also accepts a prepared
case_dir.
Returns the degradation dict or {job_id, status, cache_hit}; poll job_result for {ok, u_max_over_mean (≈1.5), nu_fit, nu_exact, nu_ratio (≈1, ±10 %), nu_slug, reynolds, prandtl, pe_cell, case_dir}.
Inputs: nx, ny, sif, gap_m, +10 more.
classify_face_sidesClassify Face SidesARead-only
Inside-vs-outside topology: for every face, decide whether its outward side opens into an enclosed cavity (wetted) or ambient (exterior). Answers the "which faces are inside the airflow path" question from issue #19 and suggests a role per face. Inspection only; returns no handle, mutates nothing.
With seal_ports=True (default) any declared inlet/outlet roles (annotate_face) are capped first, so an OPEN duct's bore reads as the enclosed flow cavity rather than as ambient.
handle: the part. seal_ports: cap declared inlet/outlet before classifying.
Returns a list (one per face) of dicts: tag / index (str) stable f_* tag and 'FaceN' kind (str) surface kind (planar/cylindrical/…) side (str) 'interior' | 'ambient' | 'ambiguous' suggested_role (str) 'wetted' for interior, 'ambient' for exterior, else null declared_role (str) the role already annotated on this face, if any
Inputs: handle (required), workspace, seal_ports.
close_documentClose DocumentADestructive
Close a document by name (or 'active' for the currently active one). Frees its objects and invalidates any handles into the closed doc. Returns {closed, invalidated_handles}.
Inputs: name, workspace.
close_sketchClose SketchCRead-only
Recompute and report DOF status. Returns {geometry_count, constraint_count, open_vertices, fully_constrained}.
Inputs: sketch (required), workspace.
close_workspaceClose WorkspaceADestructive
Shut down a workspace's worker and free its pool slot. All of that workspace's documents, unsaved changes, and handles are lost. The default workspace can be closed too (its worker respawns clean on next use).
If name is the use_workspace selection, the selection returns to "default",
whoever closes it. A call pinned with workspace never moves the selection
otherwise: closing your own pinned workspace leaves the pin as it is, so your
next call pinned to that name gets a fresh, empty worker (old handles fail).
current is the workspace this call ran in (as in list_workspaces), selected
the server-wide selection after the close.
Returns {closed: , workspace: , current: , selected: }.
Inputs: name (required), workspace.
cnc_machinability_checkCNC Machinability CheckARead-only
3-axis CNC machinability screen off a live solid — pure geometry, NO CAM engine (no toolpath, no gouge check, no holder collision).
Counts SETUPS from a tool-approach census: for every machined face, which of ±X/±Y/±Z can both address it (the normal does not point away — a wall parallel to the tool axis is milled by the cutter's periphery) and reach it (a ray from the face escapes the solid, the same caster the moldability undercut check uses), reduced to a minimum cover. Faces lying on the stock envelope are excluded: they are billet surfaces, and counting them would quote six setups for a plain block.
Findings: 'undercut' (a machined face no principal approach reaches — 5-axis, a
special cutter, or a redesign), 'deep_pocket' (depth/(2·corner radius) past
max_l_over_d — the corner radius caps the cutter and it cannot reach),
'small_radius' (an internal corner below the smallest cutter quoted, including a
SHARP planar corner reported as radius 0, which no rotating tool can produce),
'thin_wall'. pass is false when any fires; more than max_setups is a warning,
not a failure. fidelity='correlation', band_pct=None (an ordinal screen — rank
variants with score, don't gate on it).
Returns {setups, setup_directions, coverage, machined_faces, stock_faces, machined_area_mm2, min_internal_radius_mm, max_l_over_d_seen, undercut_faces, findings:[{code, severity, feature, detail}], warnings, score, pass, fidelity, band_pct, basis, escalate_to='cnc_time_estimate', limitations, n_faces}.
Inputs: model (required), workspace, max_setups, min_wall_mm, +2 more.
cnc_time_estimateCNC Time EstimateARead-only
Machining time for a live solid from a material-removal-rate model — the
honest cnc machine time cost_estimate's flat volume table cannot give.
stock = bbox grown by stock_allowance_mm per side
roughing_min = (stock − part volume) / MRR(material)
finishing_min = machined face area / finish area-rate(material)
total = (roughing + finishing)/utilisation · tolerance factor
+ setups · setup_minMRR is per material class (aluminium 60, steel 12, stainless 6, titanium 2.5
cm³/min …) — ratios that track the standard machinability ratings amplified by
the depth-of-cut headroom a soft alloy allows on the same spindle. Stock-envelope
faces are excluded from the finishing area: facing a billet is not finishing a
pocket. setups defaults to the count cnc_machinability_check derives from the
same solid, so the two tiers agree. tolerance_class ('IT7', 7, …) scales the
cutting time through the shared tolerance-cost corpus, so a tolerance costs the
same here as in tolerance_cost_check.
fidelity='correlation', band_pct=50 — the RSS of MRR scatter (±40 %) and
cut-time utilisation scatter (±25 %), against the flat table's ±100 %; supply a
measured mrr_cm3_min and it tightens to 30. Feed machine_time_hr into
cost_estimate(machine_time_hr=…) to replace the table there too.
Returns {machine_time_min, machine_time_hr, roughing_min, finishing_min, cutting_min, setup_min_total, removed_volume_mm3, stock_volume_mm3, removal_fraction, machined_area_mm2, setups, setups_basis, material_class, material_basis, mrr_cm3_min, finish_cm2_min, utilisation, tolerance, bbox_mm, part_volume_mm3, fidelity, band_pct, basis, warnings, next}.
Inputs: model (required), material (required), setups, setup_min, +6 more.
component_contract_checkComponent Contract CheckARead-only
Builder-side contract gate for one component (issue #169) — the local half of
the gate merge_assembly re-runs at fan-in. Any MCP host builds a component with
the full AnkusDrive tool surface, then calls this on its part BEFORE saving,
against its builder brief (a ankusdrive.builder_brief/1 slice), and repairs any
failing check. Catching a violation here turns the expensive loop (build → merge
→ gate-fail → rebuild) into a cheap local one. Never raises on a failing check.
handle: the component's shaped object. brief: a builder brief. Only three of its keys drive checks (the rest guide the build, not the gate): envelope {min:[x,y,z], max:[x,y,z]} the part's LOCAL bbox must fit inside. interfaces {name: {origin?:[x,y,z], z_axis?:[x,y,z], tol_mm?, angle_tol_deg?}} each named frame must be PUBLISHED (publish_interface) with a sane frame, and within tolerance of a pinned origin/axis if the brief gives one. performance {requirements?: [{name, limit}], required?: bool} the quantitative spec (#226) the builder must DECLARE (declare_performance, no looser than briefed) and PROVE (verify_performance) before fan-in.
Checks run: watertight (check_shape's one-clean-solid verdict), envelope (local bbox inside the keep-out box), interface: (published + sane + in tol), performance_spec: (declared as briefed) and performance: (the last RECORDED verify_performance verdict says it is met).
A performance requirement the record says is NOT met fails the gate. One with no
verdict yet — never verified, a solve still in flight, or a verdict invalidated by a
later edit — is neither passed nor failed: it comes back in skipped with a reason,
because "unverified" is not "fine" and must not be actioned as either.
Returns {handle, ok, checks:[{check, passed, detail}], reasons:[...],
skipped:[{check, reason}], performance?} — ok True iff every check passed (skips
never move it); reasons is the failing checks' details. A part that declares no
performance contract gets no performance rows, empty skipped and no performance
key, so the geometric gate is unchanged.
Inputs: brief (required), handle (required), workspace.
contact_setupContact SetupA
Set up surface-to-surface contact between face pairs for a CalculiX solve and
flip the solver to nonlinear — no new solver (promotes the CCX contact/nonlinear
flags the FEM path already exposes). face_pairs is a list of
{a:{handle, tag|face}, b:{handle, tag|face}} (master, slave) pairs; friction is
the Coulomb coefficient (0 = frictionless); slope optionally sets the penalty
contact stiffness; nonlinear (default True) sets the solver's
GeometricalNonlinearity.
Run fem_run + fem_results after. Gate RELATIVE to a bonded reference on the same mesh: a bonded model is stiffer (less peak displacement) than frictional contact. Returns {contacts:[handles], n_pairs, friction, nonlinear}.
Inputs: analysis (required), face_pairs (required), name, slope, +3 more.
copy_shapeCopy ShapeA
Duplicate a shaped object as an INDEPENDENT static solid.
Unlike add_part (which creates an App::Link that tracks the source), this deep-copies the geometry: later edits to the original do NOT propagate to the copy. Use it to seed a mirror/pattern, or to drop a standalone duplicate instance into an assembly.
handle: handle of the source object (must have a Shape). placement: optional absolute [x, y, z] translation in mm applied to the copy's base. Omit to leave the copy coincident with the source. The source object is unchanged and stays visible. name: optional name for the new object (default '_copy').
Returns {handle, name, volume}: handle is a new 'copy_N' handle, name is the FreeCAD object name, volume is the copied solid's volume in mm^3.
Inputs: handle (required), name, placement, workspace.
cost_estimateCost EstimateARead-only
Per-unit cost rollup (Design for Cost). material_cost = volume·density·price ·(1+scrap) from the Materials DB (process: cnc | fdm | casting | injection). process_cost = amortized setup + per-process machine time; tooling amortized over quantity, so unit_cost falls as quantity rises. The machine-time table is order-of-magnitude (fidelity='correlation', band_pct=100) — trust the ratios between processes/quantities, not the absolute dollars; material_cost alone is exact given its inputs.
material may be any Materials-DB card name (material_list / material_get), or
anything at all if you price it yourself: price_usd_kg and density_kg_m3
override the DB lookup and are flagged in breakdown.price_basis /
density_basis as 'explicit' instead of 'material'. A generic word like
'aluminum' is a CATEGORY, not a card — it carries a density but no price, so it
needs price_usd_kg or a real card name (AL6061-T6, …); the error says which.
Two optional inputs sharpen it. tolerance_class ('IT7', '9', …) scales the
TABLE machine time by the tolerance-cost curve (see tolerance_cost_check):
holding tighter than the process's natural capability roughly doubles cost every
1.5 IT grades. machine_time_hr REPLACES the table with a time a real model
computed (cnc_time_estimate) and drops band_pct from 100 to that model's band
(50 by default, or machine_time_band_pct); the tolerance factor is then not
applied again, because cnc_time_estimate already applied the same curve.
Both default to None, reproducing the pre-existing behaviour exactly.
Returns {material_cost, process_cost, tooling_amortized, unit_cost, mass_kg, fidelity, band_pct, breakdown:{…, density_kg_m3, price_usd_kg, density_basis, price_basis, machine_time_hr, machine_time_basis, base_machine_time_hr, tolerance_class, tolerance_factor, tolerance_applied, tolerance_basis}}. Errors on a material with no usable density/price and no override, an unknown process/tolerance class, or a non-positive volume/quantity/machine time.
Inputs: material (required), volume_mm3 (required), process, quantity, +10 more.
creep_flagCreep Risk FlagARead-only
Screen for creep risk: compare operating temperature to the material's max service temperature (Materials DB, or an override). A screen, not a Larson-Miller life model. pass = below the service limit. Returns {operating_temp_c, service_temp_c, margin_c, stress_mpa, creep_risk, pass, reason}.
Inputs: temp_c (required), stress_mpa (required), material, workspace, +1 more.
declare_intentDeclare IntentADestructive
Record the functional invariants a part must keep satisfying, so they can be re-checked after every edit (see verify_intent). Persists in the .FCStd as a JSON property bag (AD_Intent); one contract per part — re-declaring replaces.
handle: the part. contract: a dict with any of these (declare at least one): watertight (bool) require check_shape's watertight_solid verdict. airtight_path (dict) {inlet, outlet, min_aperture_mm2?}; each port is a face tag / 'FaceN' / int / declared role-or-name. required_faces (list) face tags / 'FaceN' / declared role-or-names that must still resolve (catches a deleted/drifted face).
Returns {handle, contract} — the stored contract.
Inputs: handle (required), contract (required), workspace.
declare_performanceDeclare PerformanceADestructive
Record a quantitative PERFORMANCE spec on a part so it can be re-proved after every edit — the performance twin of declare_intent, which only covers geometry. Persists in the .FCStd as a JSON property bag; one contract per part, re-declaring replaces it.
Each requirement is metric-agnostic — the contract layer only orchestrates, so the metric is whatever the named tool already returns, and "Cd ≤ 0.30", "Δp ≤ 50 Pa", "first mode ≥ 200 Hz" and "ΔT ≤ 40 K" are the same machinery:
{"name": "drag_at_cruise",
"metric": "cd", # dotted path into the tool's result
"tool": "cfd_external_flow_submit", # what measures it at solver tier
"conditions": {"model": "$handle", "velocity_m_s": 30, "fluid": "air-20c"},
"limit": {"max": 0.30}, # max, min, or both (a window)
"screen": {"tool": "cfd_body_drag", "metric": "cd",
"conditions": {"shape": "sphere", "diameter_mm": 50,
"velocity_m_s": 30}},
"fidelity_floor": "solver", # "screen" if an estimate is proof enough
"trust": {"converged": true, "band_max_pct": 5}}"$handle" anywhere in conditions is replaced with this part's handle at
verification time, so a contract is portable between parts. trust demands are
enforced by verify_performance against the solver's own trust block: a requirement
asking for converged: true can never be satisfied by an unconverged solve.
Returns {handle, contract: {requirements: [...]}, n_requirements}. Raises ValueError on a malformed requirement, naming the offending one.
Inputs: handle (required), requirements (required), workspace.
dem_flow_submitDEM Flow SubmitA
Discharge spheres from a flat-bottomed hopper box through a central orifice
with the REAL YADE discrete-element engine and measure the steady mass-flow
rate — gated against granular_screen('beverloo') (flow ∝ outlet^2.5). Submit
two outlet_m sizes and feed the (outlet, flow) pair to
granular_screen('beverloo_exponent') to check the Beverloo 2.5 exponent (vs the
Torricelli 2.0 of a draining fluid). YADE is GPL-3.0 and is run ONLY in a
subprocess; runs OFF the MCP channel via a background job.
box_m is the [Lx, Ly, Lz] hopper box (m; default [0.10, 0.10, 0.20]);
outlet_m the central orifice diameter; settle_steps/flow_steps the DEM
step budgets. Returns {job_id, status, cache_hit, oracle}; the job result
carries the Beverloo oracle PLUS {mass_flow_kg_s, n_discharged,
discharge_time_s, positions:[...]}. Absent YADE: {ok:false, reason, install,
oracle}.
Inputs: box_m, density, outlet_m, radius_m, +6 more.
dem_pack_submitDEM Pack SubmitA
Pour N monodisperse spheres into a box and settle them under gravity with the REAL YADE discrete-element engine, then measure the random close-packing fraction φ of the settled bed — gated against granular_screen('packing') (RCP band 0.60–0.66, well below the crystalline 0.7405). YADE is GPL-3.0 and is run ONLY in a subprocess (the parent never imports it — same arm's-length isolation as the GPL Elmer/OpenFOAM binaries). Runs OFF the MCP channel via a background job, so a multi-second settle never blocks the worker.
box_m is the [Lx, Ly] floor footprint (m; default [0.06, 0.06]); the column
height is sized to hold n_spheres. friction_deg is the inter-particle
friction angle; young_pa the contact modulus; density the grain density.
Returns {job_id, status, cache_hit, oracle} (poll job_status / job_result);
the job result carries the oracle band PLUS the measured {packing_fraction,
packing_fraction_bulk, in_band, gated_on, gated_fraction, n_settled,
settled_height_m, mean_coordination, positions:[[x,y,z,r]]}. in_band judges
the wall-free bulk φ (the whole-bed φ is deflated by loose wall and surface
layers); it falls back to the whole-bed φ only when the bed is too small to
have a bulk region, and gated_on names which fraction was judged.
Absent YADE: {ok:false, reason, install, oracle}.
Inputs: box_m, steps, density, radius_m, +4 more.
designation_checkDesignation CheckARead-only
Gate: every purchased part on this assembly must be orderable.
Flags BOM rows a buyer cannot act on — a purchased standard part with no canonical designation, or one whose designation is missing a fact needed to order it (no material grade). Run it before quoting or releasing a package: a BOM whose purchased lines are geometry names silently pushes the sourcing work onto a human, once per revision.
Purchased-ness is EXACT for AnkusDrive-generated parts (the object carries a stamp)
and a documented name heuristic for everything else — so a hand-modelled
"Bracket" is never flagged, while a hand-modelled "M6Screw" is. Pass rows
instead of assembly to check BOM rows you already hold.
Returns {ok, findings, purchased, designated, undesignated, incomplete, basis}; each finding carries part / count / code (no_designation | incomplete_designation) / certainty (stamped | name_heuristic) / reason. ok=False means the BOM cannot be ordered as it stands.
Inputs: rows, assembly, recursive, workspace.
dfa_checkDFA CheckARead-only
Grade an assembly (Boothroyd-Dewhurst-lite). assembly_efficiency = theoretical_min/(part_count+fastener_count) (theoretical_min = unique_part_count or 1); assembly_score scales that by a handling penalty from insertion_axes/ symmetry and decreases monotonically as part/fastener count rises. The grade is an ordinal index for comparing variants (fidelity='correlation', band_pct=None) — rank with it, don't gate on the absolute value. Returns {part_count, fastener_count, insertion_axes, handling_difficulty, assembly_efficiency, assembly_score, symmetry_score, fidelity, band_pct}.
Inputs: part_count (required), workspace, fastener_count, insertion_axes, +2 more.
dfm_checkDFM CheckARead-only
Screen a part for manufacturability against a pull/tool axis. Give a
hand-built faces list of {name, draft_deg, wall_mm?} — draft_deg relative to
pull_axis (0 = a vertical wall needing draft; <0 = a re-entrant undercut) — OR
a live handle, whose per-face descriptors are read off the solid (draft vs
the pull axis + a ray-cast undercut test + inward-chord wall sampling) and
scored identically (v2 Shape wiring). draft_violations are 0≤draft<min_draft_deg,
undercut_faces are draft<0, min_wall_violations are wall_mm<min_wall_mm
(defaults by process: injection 1.0, cnc 0.5, sheet/fdm 0.8).
Sheet metal: a handle built by sheet_base/sheet_flange/sheet_tab/sheet_hem is
ALSO screened against the press-brake rules (minimum bend radius by material,
minimum flange length, hole-to-bend distance, refold collision) with no extra
argument — those rules are DELEGATED to the same implementation sheet_check
calls, so the two tools cannot return different verdicts on one part. Pass an
explicit sheet block {thickness_mm, material?, bends, holes?, interferences?}
to screen bends on a part AnkusDrive did not model.
Returns {process, pull_axis, min_wall_mm, draft_violations, undercut_faces,
min_wall_violations, score, pass} — plus n_faces + wall_thickness_stats on the
handle path, and a sheet sub-result {ok, findings, rules, fidelity, band_pct}
whose failures also gate pass on the sheet-metal path.
Inputs: faces, sheet, handle, process, +4 more.
dipole_resonanceDipole ResonanceARead-only
Thin centre-fed half-wave dipole first resonance (NO solver, banded) — the
closed-form twin the openEMS FDTD S11 antenna sweep (em_fullwave_submit) is
gated against. Give EXACTLY ONE of length_mm (→ resonant frequency) or
freq_ghz (→ resonant length). End-effect shortening k = shortening makes
the resonant length a little under λ/2: L = k·λ, f_r = k·c/L (k≈0.48 textbook;
≈0.475 typical wire). Because k tracks the length/diameter ratio this is a
±band correlation (fidelity='banded', ~±3% over k∈[0.46,0.49]); an FDTD S11
sweep must put its first resonance inside [freq_lo, freq_hi] (or [length_lo,
length_hi]).
Returns {given, shortening, half_wavelength_mm, resonant_length_mm, resonant_freq_ghz, freq_lo_ghz, freq_hi_ghz, length_lo_mm, length_hi_mm, fidelity, band_pct, valid_range_ok, warnings, escalate_to}.
Inputs: freq_ghz, length_mm, workspace, shortening.
draftDraftA
Apply a draft angle to faces (for moldability).
base: feature handle. faces: list of {handle, face: tag|'FaceN'}. angle_deg: draft angle (positive degrees). neutral_plane: {handle, face: tag|'FaceN'} for the plane along which the angle is measured (typically the parting plane). reversed: flip the direction of the draft.
Inputs: base (required), faces (required), name, reversed, +3 more.
drawing_gateDrawing GateARead-only
Manufacturing-completeness gate for a drawing page: do the placed dimensions fully and non-redundantly reconstruct the part? Reads the real solid + placed dimensions and accounts degrees of freedom, process-aware: 'prismatic' needs each hole located X/Y from a datum and the block sized W×H×T; 'turned' needs only Ø + axial length per step. process='auto' infers it.
Datum discipline (a location not measured from a datum face → no_datum) turns on when faces carry role='datum' (annotate_face), or with datums_declared=True. require_ballooned=True also fails unballooned characteristics (not_ballooned; see balloon_drawing), as a release flow does.
Returns {ok, violations, slots_total, slots_covered, process, features,
dimensions, enumerated_features, datum_faces, section_recommended ({recommended,
reasons, feature_ids}; see add_section_view), ballooned ({ok, total, ballooned,
missing})}. Each violation has a code (under = size/location missing;
redundant = DOF dimensioned twice; conflict = twice with disagreeing values;
extra = pins nothing; no_datum; not_ballooned) and a reason. ok=True means
manufacturing-complete.
Inputs: page (required), process, workspace, datums_declared, +1 more.
drawing_legibilityDrawing LegibilityARead-only
Legibility gate for a drawing page: on the ACTUAL placed graphics, flag the ways the layout becomes unreadable — overlapping dimension labels, a dimension line crossing a view it does not reference, or anything past the sheet border.
min_gap (mm) is the breathing room required between two labels. Returns {ok,
violations, labels, segments, views}; each violation has a code
(overlap/crosses_view/out_of_border) and a human reason. ok=True means the
placed dimensions read cleanly on the sheet. Inspection balloons count as placed
graphics too — a numbered circle sitting on a neighbour or off the sheet is
flagged like any other label.
Inputs: page (required), min_gap, workspace.
drop_impactDrop ImpactARead-only
Drop/impact screen by exact energy balance (NO solver). Give
crush_distance_mm (available cushion/crumple stroke) -> deceleration, OR
deceleration_limit_g (fragility spec) -> required stroke — exactly one.
G_avg = h/d exactly (mass cancels); g_peak = pulse_factor·G_avg with pulse
bounding the shape: 'constant' (ideal crush, 1×) | 'linear_spring' (elastic,
2×) | 'half_sine' (π/2×). v = √(2gh). mass_g only adds peak_force_n and
energy_j. fidelity='exact'; where the part is stressed, or what an edge/corner
strike changes, is escalate_to='impact_dynamics_submit'.
Returns {drop_height_mm, impact_velocity_m_s, pulse, pulse_factor, crush_distance_mm, g_avg, g_peak, pulse_duration_ms, deceleration_limit_g, required_crush_mm, energy_j, peak_force_n, fidelity, band_pct, valid_range_ok, warnings, escalate_to}.
Inputs: drop_height_mm (required), pulse, mass_g, workspace, +2 more.
eco_createECO CreateADestructive
Build an ECO change-order record (issue #142, C3) — turn a change into a record, not a silent mutation (the diff IS the change order, mapping onto a git commit/PR). Optionally compute its where-used impact over a lockfile in the same call, so the blast radius travels with the record.
id: the ECO id (e.g. "ECO-0001").
affected: the list of changed item / component ids.
disposition: the change disposition (use_as_is | rework | scrap | revise | ...).
effectivity: a dict with exactly one of date|serial|revision (when it takes
effect).
title / note: optional human description recorded on the record.
interface_change: true if a published interface moved (the §9 stale trigger).
lockfile: optional path — when given, the result embeds an impact report.
out: optional path — write the git-diffable ECO sidecar there.
Returns the ECO object (with impact when a lockfile is supplied); a malformed
ECO fails loudly.
Inputs: id (required), affected (required), disposition (required), effectivity (required), +6 more.
eco_validateECO ValidateARead-only
Validate an ECO (engineering change order) record (issue #142, C3) — the cheap front door. Checks the schema stamp, a non-empty id + affected item set, a present disposition, and an effectivity carrying exactly one of date|serial|revision.
eco: the ECO object.
Returns {ok, problems} — ok is True iff problems is empty.
Inputs: eco (required), workspace.
elastica_deflectionElastica DeflectionARead-only
Exact large-deflection cantilever tip — Bisshopp–Drucker elastica (NO
solver) — the closed-form twin the *NLGEOM CalculiX solve is gated against.
Section: width_mm+height_mm (solid rectangle, I = b·h³/12, load transverse to
height_mm) or explicit i_mm4. E from youngs_gpa or a Materials-DB material.
Load parameter α = P·L²/(E·I); the tip slope solves the elliptic-integral
elastica. Linear theory δ/L = α/3 over-predicts the transverse tip and ignores
the axial draw-in — the elastica captures both, and nonlinear_over_linear is
the divergence the solve must reproduce. Valid for tip slope < ~80° (α ≲ 3.5);
beyond that escalate to a follower-load fem_set_nonlinear_material solve.
Returns {alpha, tip_slope_deg, tip_disp_mm (transverse), tip_x_mm (axial projection), axial_drawin_mm, linear_tip_mm, nonlinear_over_linear, youngs_mpa, I_mm4, fidelity, band_pct, valid_range_ok, warnings, escalate_to}.
Inputs: load_n (required), length_mm (required), i_mm4, material, +4 more.
em_conduction_submitEM Conduction SubmitA
DC current conduction via Elmer's StatCurrentSolver (P3 M6 frontier),
asynchronous. Requires ElmerSolver; when absent this returns {ok:false, reason,
install} rather than raising. Builds a rectangular strip with voltage_v
across its ends and reads the electrode current, total Joule heating and
Elmer's effective resistance — all machine-exact against R = L/(σ·A)
(resistance_ratio = 1.000000 live).
Returns the degradation dict or {job_id, status, cache_hit}; poll job_result for {ok, current_a, joule_w, effective_resistance_ohm, resistance_exact_ohm, current_exact_a, resistance_ratio, case_dir}.
Inputs: nx, ny, width_m, length_m, +4 more.
em_dc_resistanceEM DC ResistanceARead-only
Exact DC resistance of a uniform conductor (NO solver) — R = L/(σ·A), the
closed-form anchor the Elmer em_conduction_submit gate reproduces to machine
precision. With voltage_v the Ohm/Joule pair is included (I = V/R, P = V·I).
σ from conductivity_s_m or a conductor name.
Returns {resistance_ohm, conductivity_s_m, length_m, area_m2, current_a?, joule_w?}.
Inputs: area_mm2 (required), length_mm (required), conductor, voltage_v, +2 more.
em_fieldEM FieldARead-only
Exact magnetostatic field of the two canonical sources (NO solver):
kind='wire' is the long straight wire B = μ₀·I/(2π·r) at distance_mm
(Ampère's law); kind='solenoid' is the long-solenoid interior
B = μ₀·μ_r·n·I with turns_per_m.
Returns {b_t, b_mt, …} (the field in tesla and millitesla).
Inputs: kind, mu_r, current_a, workspace, +2 more.
em_fullwave_submitEM Fullwave SubmitA
Full-wave FDTD EM solve on openEMS, asynchronous (OFF the MCP channel) — the
real-field twin of the analytic waveguide_cutoff / dipole_resonance
oracles. openEMS is GPL-3.0 and is run ONLY out-of-process via
ankusdrive/em_fullwave_gpl_runner.py; degrades to {ok:false, reason, install}
when no openEMS venv resolves.
problem='waveguide_sweep' (default): hollow rectangular guide, broad wall
a_mm/narrow wall b_mm (default a/2), length length_mm (default 120), TE10
port at each end. Sweep f_start_ghz..f_stop_ghz (default 4..10 GHz —
straddling the WR-90 cutoff 6.56 GHz) in n_freq points, nrts max timesteps,
cells_per_wl mesh density, eps_r fill, end_criteria energy stop (1e-6).
THE GATE IS alpha_ratio≈1: voltage probes along the guide read the SOLVED
field's decay rate below cutoff at decay_f_ratio·f_c (default 0.7) against
the exact α = sqrt((π/a)² − k²); it degrades under a coarse mesh or a truncated
nrts. alpha_ratio is null with a decay_note when the guide is too short
for the probe window. fc_ratio (half-power crossing vs c/2a) is only a
PORT-SETUP CHECK: openEMS's analytic port β zeroes every sub-cutoff sample, so
it reads the frequency grid, not the field. A degenerate mesh/length (port
blocks overlapping) returns {ok:false, error}.
problem='dipole_s11': centre-fed thin dipole (length_mm, gap_mm,
radius_mm, each resolved by its own mesh lines), sweep S11, report first
resonance. Mesh is mesh_res_mm, else λ(mesh_f_ghz, default f_stop) /
cells_per_wl (default 30) — pin it to vary the sweep window alone.
Returns the degradation dict, or {job_id, status, cache_hit}; poll job_result for {ok, fc_analytic_ghz, freq_ghz[], s21_db[], transmission_norm[], alpha_fdtd, alpha_exact, alpha_ratio, decay_freq_ghz, decay_probe_z_mm[], decay_fit_rms_np, fc_crossing_ghz, fc_ratio, evanescent_mean, propagating_mean, mesh_res_mm, n_cells, wall_s} (waveguide) or {freq_ghz[], s11_db[], resonance_ghz, resonance_s11_db, mesh_res_mm, n_cells} (dipole).
Inputs: a_mm, b_mm, nrts, eps_r, +14 more.
em_induction_heating_submitEM Induction Heating SubmitADestructive
Coupled induction heating via Elmer (SIMULATION_NEXT B5), asynchronous —
completes em_induction_submit into a THERMAL answer: the harmonic
MagnetoDynamics solve runs once, MagnetoDynamicsCalcFields turns it into the
time-averaged Joule loss field, and a transient adiabatic HeatSolver
integrates it for heat_duration_s. Requires ElmerSolver; when absent this
returns {ok:false, reason, install} rather than raising.
Two gates: joule_power_ratio — the solved eddy-current power vs the exact
deep-slab dissipation P″ = R_s·|H₀|²/2 = ω²σA₀²δ/4 (from the shipped
em_skin_depth chain; live 1.0003) — and energy_balance_ratio — the mean
temperature rise vs P·t/(m·cₚ) (live 1.005). Conductor σ from a name or
explicit conductivity_s_m; thermal ρ/cₚ/k explicit. Also accepts a prepared
case_dir.
Returns the degradation dict or {job_id, status, cache_hit}; poll job_result for {ok, eddy_power_w_m, p_total_exact_w_m, joule_power_ratio (≈1), t_mean_final_k, dt_mean_exact_k, energy_balance_ratio (≈1), skin_depth_m, case_dir}.
Inputs: nx, ny, sif, mu_r, +12 more.
em_induction_submitEM Induction SubmitA
AC skin effect / induction via Elmer's harmonic 2-D magnetodynamics (P3 M6
frontier), asynchronous. Requires ElmerSolver; when absent this returns
{ok:false, reason, install} rather than raising. Builds a conductor slab
depths skin depths deep driven by the surface vector potential at
frequency_hz, solves the complex field, and fits the e-folding length of
BOTH the magnitude and the phase of A(x) — each must equal the exact
δ = √(2/(ω·μ₀·μ_r·σ)) (live: decay_ratio 0.999, phase_ratio 1.000). The Joule
deposition profile |J|² ∝ e^(−2x/δ) is the induction-heating answer.
Returns the degradation dict or {job_id, status, cache_hit}; poll job_result for {ok, skin_depth_exact_m, decay_length_m, phase_length_m, decay_ratio, phase_ratio, case_dir}.
Inputs: nx, ny, mu_r, depths, +4 more.
em_skin_depthEM Skin DepthARead-only
Exact AC skin depth (NO solver) — δ = √(2/(ω·μ₀·μ_r·σ)) plus the per-square
surface resistance R_s = 1/(σ·δ); fields/current decay e^(−x/δ) into the
conductor (~95% of induction heating deposits within 1.5·δ). σ from an explicit
conductivity_s_m or a conductor name (copper, aluminum, silver, gold, brass,
steel-mild, stainless-304).
Returns {skin_depth_m, skin_depth_mm, surface_resistance_ohm, angular_frequency_rad_s, conductivity_s_m, mu_r}.
Inputs: frequency_hz (required), mu_r, conductor, workspace, +1 more.
engrave_textEngrave TextA
Engrave (cut) or emboss (add) extruded text on a planar face of a solid, centred on the face centroid. The host is replaced by the returned handle.
face: planar face — f_* tag (preferred), 'FaceN', or int. size: cap height mm. depth: engrave/emboss depth mm. mode: 'engrave' (removes material) | 'emboss' (adds). position: optional [u, v] mm offset from the centroid along the text's X/Y. font: absolute path to a .ttf/.ttc. Omitted: only macOS Arial/Helvetica are probed, so elsewhere it raises — pass a path.
Returns {handle, name, volume (mm^3 of the result), text, mode, depth}.
Inputs: face (required), text (required), handle (required), font, +6 more.
envelope_checkEnvelope CheckARead-only
Keep-out gate: assert each named part's world bounding box stays inside its declared envelope. envelopes maps a part's link name (or label) to {"min": [x,y,z], "max": [x,y,z]} in the assembly frame. Returns violations [{part, axis, got, allowed}, ...]; empty means everything is within its box.
Inputs: assembly (required), envelopes (required), workspace.
export_drawingExport DrawingBDestructive
Export a drawing page to PDF, SVG, or DXF (format inferred from the path extension), headless. PDF/SVG are composed from the template, the per-view geometry, and any dimensions/annotations on the page; DXF uses FreeCAD's native page writer. Returns {path, size, format, views, dimensions}.
Inputs: page (required), path (required), workspace.
export_shapeExport ShapeADestructive
Export a shape to STEP/IGES/BREP/STL. Format inferred from path extension.
object: FreeCAD object name (NOT a AnkusDrive handle). If omitted, exports the document's single final shape: inputs consumed by another feature (a cut's base/tool, Body features) are skipped, and solids win over sketches. If more than one final shape remains, it raises and lists them. It never picks one for you.
Inputs: path (required), object, workspace.
fai_reportFAI ReportADestructive
First-article inspection report for a drawing page, shaped like AS9102 Rev B Form 3: one row per ballooned characteristic with the Form 3 fields, limits, suggested method, and computed status.
results: balloon number -> measured value; rows are accepted/rejected against
limits. For position pass {"x":.., "y":..} (deviation 2·√(x²+y²), as
gdt_check). Omit for a BLANK form: every row 'not_evaluated', never a pass.
path: optionally write .csv (data), .svg or .pdf (printable table).
part / rev: identity stamped in; default page part name / title-block rev.
reference: AS9102 field 6 (Reference Location); defaults to each
characteristic's view.
NOT a certified AS9102 submission (every artifact says so).
Returns {ok, columns, rows, summary, disclaimer, part, rev, plan_ok, unmeasurable, path?, size?, format?}; ok=False means a characteristic measured out of limits.
Inputs: page (required), rev, part, path, +4 more.
family_materializeFamily MaterializeADestructive
Materialize a whole variant family from ONE design table (issue #138, B1) — "make all the gears" becomes a table, not a loop. For each row, in table order, builds the part with its recipe (A1, #136) and allocates one item + one sequential part number (C1, #140). Subsumes standard-part catalogs: a bearing catalog is a family table keyed by designation, sourced from the ISO corpus.
Two modes (both supported): "instances" (each variant its own released file + part number) and "configurations" (variants share one artifact). Builds into the active document. Returns {schema, family, recipe, mode, key, count, rows, registry}.
table: path to the family table (.csv or .json). registry: optional items.json path (created if absent, written back). mode: optional override of the table's mode (instances|configurations).
Inputs: table (required), mode, registry, workspace.
family_validateFamily ValidateARead-only
Validate a variant-family design table (issue #138, B1) — a row x column table where row = a variant (keyed by a size designator) and column = a recipe parameter / feature-flag / material. Loads CSV or JSON and checks the recipe, mode, key column, duplicate/missing size keys, and every per-row recipe-door value; each problem names the row+column. Returns {ok, problems}.
table: path to the family table (.csv or .json).
Inputs: table (required), workspace.
fatigue_checkFatigue CheckARead-only
Rate fatigue life (S-N Basquin + Goodman mean-stress correction). σ_a =
stress_range/2; infinite-life SF = 1/(σ_a/σ_e + σ_m/σ_uts); finite life from
an equivalent fully-reversed amplitude on a log-log S-N line. σ_e/σ_uts come
from the material (or overrides). pass = survives cycles (σ_ar ≤ σ_e ⇒
infinite life); a tensile mean ≥ σ_uts fails outright. Returns
{stress_amplitude_mpa, mean_stress_mpa, endurance_mpa, uts_mpa,
equiv_reversed_mpa, safety_factor, life_cycles, required_cycles, pass,
governing_mode, endurance_basis}.
The S-N line runs from (1e3, s1000_fraction·UTS) to (endurance_cycles, σ_e).
Both default to the steel convention (0.9 and 1e6); aluminium and other
non-ferrous alloys have no true endurance knee, so set endurance_cycles to the
life the quoted σ_e was measured at (commonly 5e8).
Inputs: stress_range_mpa (required), cycles, uts_mpa, material, +5 more.
feature_instantiateFeature InstantiateADestructive
Stamp a registered FEATURE TEMPLATE onto a host body at reference geometry supplied BY NAME (PowerCopy/UDF, issue #139) — e.g. a mounting_boss onto a published seat frame, or a bolt_pattern onto an f_ face tag. Validates {refs, inputs} at the door, resolves each reference against the host's CURRENT geometry (an f_ tag / interface name that doesn't resolve fails loudly), then runs the deterministic build — geometry + publish_interface + declare_intent.
template: registered template name (feature_list to browse). host: the handle of the body to stamp onto. refs: reference inputs by name — an interface name, an f_/e_ tag, or a literal {origin, z_axis?, x_axis?} frame, per the template's declared ref kinds. inputs: scalar parameters (typed + unit-bearing + range-checked).
Returns {template, schema, host, refs, inputs, handle, name, interfaces, intent}.
Inputs: host (required), template (required), refs, inputs, +1 more.
feature_listFeature ListARead-only
List every registered FEATURE TEMPLATE — the PowerCopy/UDF analog of a part recipe: a reusable feature with declared reference-geometry inputs (a frame, an f_/e_ tag, an axis) plus scalar parameters, stamped onto a host by name (issue #139). Returns {schema, count, templates} where each maps to {doc, refs, required, optional, emits}; the directory to browse before picking one with feature_schema / feature_instantiate.
Inputs: workspace.
feature_schemaFeature SchemaARead-only
Return one feature template's declared REF+INPUT SCHEMA — its reference geometry (name/kind) and its scalar parameters (type/unit/default/range). Returns {schema, template, doc, refs:[{name, kind, required, doc?}], inputs:[{name, type, unit?, default?, min?, max?, required, choices?, doc?}], emits}. An unknown name fails loudly.
Inputs: template (required), workspace.
feature_validateFeature ValidateARead-only
Validate a feature instantiation {template, refs, inputs} WITHOUT building it — the cheap structural front door (mirrors recipe_validate). Catches an unknown template, an unknown/missing required reference, a malformed reference value, and every scalar-input failure (missing required, out of range, wrong type, bad unit, unknown key). A tag that doesn't resolve against real geometry is caught at feature_instantiate time. Returns {ok, problems}.
Inputs: template (required), refs, inputs, workspace.
fem_add_constraintFEM Add ConstraintB
Add a constraint by face/edge tag (Slice 1).
kind: structural: 'fixed' | 'force' | 'pressure' | 'displacement' thermal: 'temperature' | 'heatflux' | 'initial_temperature' refs: list of {handle, tag} dicts; 'tag' may be a face tag (f_...) or edge tag (e_...). Resolved against the live shape so refs survive unrelated geometry edits.
For 'force': force (N), optional direction {handle, edge|tag}. For 'pressure': pressure (MPa). For 'displacement': x/y/z (mm) or x_free/y_free/z_free. For 'temperature' / 'initial_temperature': temperature (°C / K). For 'heatflux': flux_type ('DFlux'|'Convection'|'Radiation'), and DFlux: flux (W/m²); Convection: ambient_temp (°C) + film_coef (W/m²K); Radiation: ambient_temp + emissivity.
Inputs: kind (required), refs (required), analysis (required), x, +17 more.
fem_bucklingFEM BucklingBDestructive
Configure analysis for linear buckling. Apply a unit-magnitude force constraint at the load location; the result factors are the multipliers at which buckling occurs.
Inputs: analysis (required), n_factors, workspace.
fem_buckling_resultsFEM Buckling ResultsARead-only
Extract buckling load multipliers from a completed buckling run.
Pass analysis, or the job_id of a finished fem_run_submit solve.
Returns {buckling_factors: [...], modes: [{mode, factor}, ...]}.
Inputs: job_id, analysis, workspace.
fem_cantilever_demoFEM Cantilever DemoADestructive
Run the built-in cantilever FEM demo end-to-end (geometry → mesh → CalculiX).
Dimensions in mm, force in N. Returns {nodes, tets, max_displacement_mm, max_vonmises_mpa, workdir}. A fresh document is created; existing state in the session is NOT overwritten but a new document becomes active.
Inputs: force, width, height, length, +3 more.
fem_meshFEM MeshA
Create a Gmsh mesh of body, attached to analysis.
char_length: max characteristic element length in mm. 0 = let Gmsh pick. min_length: min element length in mm. Default char_length/10, which keeps sliver edges in imported CAD from exploding the mesh; 0 removes the floor. element_order: '1st' or '2nd' (quadratic). Use '2nd' for bending/modal accuracy — linear tets (C3D4) shear-lock and overstiffen thin sections (a cantilever's first natural frequency lands ~50% high with only 1-2 elements through the thickness; 2nd-order tets bring it within ~1% of beam theory). Default lets Gmsh choose. Returns {handle, name, nodes, tets, min_length}.
Inputs: body (required), analysis (required), name, workspace, +3 more.
fem_mesh_refinementFEM Mesh RefinementA
Add a local mesh-refinement region to an existing FEM mesh.
mesh: handle of the FEM mesh. refs: list of {handle, face|edge|tag} dicts identifying the elements (faces/edges) to refine on. char_length: characteristic element length (mm) on those elements; should be smaller than the global mesh setting to actually refine.
Inputs: mesh (required), refs (required), char_length (required), name, +1 more.
fem_modalFEM ModalADestructive
Configure analysis for modal (frequency) extraction.
Sets solver AnalysisType='frequency' and EigenmodesCount=n_modes. f_low / f_high (Hz) optionally bound the requested mode range. Caller still calls fem_run, then fem_modal_results to read frequencies.
Inputs: analysis (required), f_low, f_high, n_modes, +1 more.
fem_modal_resultsFEM Modal ResultsARead-only
Extract natural frequencies from a completed modal run.
Pass analysis, or the job_id of a finished fem_run_submit solve.
Returns {frequencies_hz: [...], modes: [{mode, frequency_hz, max_displacement_mm}, ...]}.
Inputs: job_id, analysis, workspace.
fem_new_analysisFEM New AnalysisB
Create a Fem::FemAnalysis container. Returns {handle, name}.
Inputs: name, workspace.
fem_result_probeFEM Result ProbeARead-only
Probe FEM results at a specific location, instead of only the global
max + top-N that fem_results returns. Answers "what is the stress at this
point / on this face?" — the agent-friendly form for design iteration.
Pick exactly one mode:
POINT: point=[x, y, z] (mm, model coordinates). Interpolates the field barycentrically inside the tet containing the point (method='interpolated'); if the point is outside the mesh it falls back to the nearest node and reports distance_mm (method='nearest_node').
FACE: handle= + face=<'f_*' tag or 'FaceN'>. Aggregates the field over the mesh nodes on that CAD face, returning {min, max, mean}.
field: 'auto' (default — every field present in the result) | 'vonmises' | 'displacement' | 'temperature'.
Pass analysis, or the job_id of a finished fem_run_submit solve.
Returns (point mode) {mode:'point', query_point, method, element_id?, node?, distance_mm, vonmises_mpa?, displacement_mm?, displacement_vector?, temperature_c?}; (face mode) {mode:'face', face, node_count, vonmises_mpa?:{min,max,mean}, displacement_mm?:{...}, temperature_c?:{...}}.
Inputs: face, field, point, handle, +3 more.
fem_resultsFEM ResultsARead-only
Extract summary results from an analysis.
Pass analysis, or the job_id of a finished fem_run_submit solve (a running or
failed job is an error that says so).
Returns {max_vonmises_mpa, max_displacement_mm, max_displacement_vector, top_stress_nodes: [{node, vonmises_mpa, displacement_mm}, ...]}.
Inputs: top_n, job_id, analysis, workspace.
fem_runFEM RunADestructive
Run the CalculiX solver on an analysis. BLOCKS the MCP channel until the solve finishes (up to 600 s).
Which to reach for: fem_run for small solves — a coarse mesh, linear static, a few thousand nodes — that finish in seconds, where a poll round-trip only adds latency. fem_run_submit for anything that may take a minute or more (fine or 2nd-order meshes, modal/buckling with many modes, nonlinear material or geometry), so the channel stays free while ccx runs.
workdir defaults to <TMPDIR>/ankusdrive_fem_<worker pid>, which is removed when
the worker exits (close_workspace, restart_worker, server shutdown); name your own
workdir to keep the .inp/.frd/.dat. Raises if ccx exits non-zero (with its *ERROR
lines), or if a fem_run_submit solve is still running on this analysis or workdir.
Returns {workdir, status: 'ok', returncode, analysis_type,
result_objects, analysis}.
Inputs: analysis (required), workdir, workspace.
fem_run_submitFEM Run SubmitADestructive
Run the CalculiX solver on an analysis OFF the MCP channel — the submit→poll form of fem_run, for solves long enough to stall the channel (see fem_run for which to use).
The solver input is written before this returns, so a missing solver, material or mesh fails here, synchronously, exactly as fem_run would. ccx then runs in the background. Poll job_status: once ccx exits, the results are imported into the analysis on your NEXT poll (FreeCAD work runs on the worker's main thread, which a poll gives a turn) — so keep polling until status is 'done'; a job nobody polls never finishes. A ccx error fails the job with its *ERROR lines.
When done, read results with fem_results / fem_result_probe / fem_modal_results /
fem_buckling_results / fem_thermal_results passing either analysis or this
job_id (don't job_result(discard=True) first if you plan to use the job_id).
While the job runs, fem_run or another submit on the same analysis or workdir is
refused. Returns {job_id, status, cache_hit}; job_result's result is fem_run's
return dict.
Inputs: analysis (required), workdir, workspace.
fem_set_materialFEM Set MaterialB
Bind a material to a body in an analysis.
material is a dict with at minimum: {"YoungsModulus": "210000 MPa", "PoissonRatio": "0.30", "Density": "7900 kg/m^3", "Name": "Steel-Generic"} Any extra keys are passed through to the FEM material card.
Inputs: body (required), analysis (required), material (required), name, +1 more.
fem_set_nonlinear_materialFEM Set Nonlinear MaterialA
Attach an elastoplastic (*PLASTIC) hardening curve to a linear FEM material
and switch the CalculiX solve to nonlinear — the material-nonlinearity half of
the nonlinear FEM path (contact_setup is the geometric/contact half). No new
solver: this promotes the CCX MaterialNonlinearity / GeometricalNonlinearity
flags the FEM path already exposes. base_material is the handle from
fem_set_material (its YoungsModulus/PoissonRatio stay the elastic branch).
Give the post-yield curve either as yield_points ([[stress_MPa,
plastic_strain], ...], first point at plastic_strain 0 = initial yield) or from
yield_mpa (+ optional tangent_modulus_mpa linear-hardening slope and
max_plastic_strain). With no tangent modulus the curve is
elastic–perfectly-plastic and caps the stress at σ_y exactly. hardening:
'isotropic' (monotonic) or 'kinematic' (cyclic/Bauschinger). Set
geometric_nonlinearity=true to combine plasticity with large deflection
(*NLGEOM). ramp_increments sub-divides the load step so ccx's plastic
return-mapping converges. Run fem_run + fem_results after; gate against
plastic_collapse (perfectly-plastic stress saturates at σ_y, collapse at M_p).
Returns {handle, name, hardening, yield_points, n_points, solver_material_nonlinear, solver_geometric_nonlinear, ramp_increments}.
Inputs: analysis (required), base_material (required), name, hardening, +7 more.
fem_set_solverFEM Set SolverA
Add a solver to an analysis. kind: 'ccx' (CalculiX) or 'elmer'.
tunables: dict of solver-property values, e.g. {"GeometricalNonlinearity": "linear", "ThermoMechSteadyState": True, "MatrixSolverType": "default"}. Sensible CCX defaults are filled in if omitted.
Inputs: analysis (required), kind, name, tunables, +1 more.
fem_thermal_resultsFEM Thermal ResultsARead-only
Extract temperature field summary from a completed thermal run.
Pass analysis, or the job_id of a finished fem_run_submit solve.
Returns {temperatures_c: {min, max, mean}, top_n_hot_nodes: [...]}.
Inputs: top_n, job_id, analysis, workspace.
fillet_edgesFillet EdgesA
Fillet edges of a shaped Part object. edges: e_* tags (preferred), 'EdgeN'
strings, or 1-based ints; radius mm (> 0).
Validated before a handle is issued (#283) — this OCC fillet can silently return corrupt geometry (a 20 mm cube, all 12 edges at r=11, comes back one LARGER solid). Checks: Shape.isValid(), unchanged solid count, no growth of the tight bounding box.
per_edge: add edges one at a time, validating each (slower; for blend-hostile geometry). allow_partial: accept a partial result instead of aborting (off by default).
Returns {handle, name, volume (mm^3), edges (indices filleted), checks {valid, solids, envelope_ok, envelope_growth_mm, envelope_tol_mm}, mode ('batch' | 'per_edge'), partial}; when partial, also skipped_edges and a warning that the solid is NOT the part asked for. On failure it retries per edge, removes the feature and raises BlendCheckFailed naming the offending edges and the subset that fillets cleanly — never a handle to corrupt geometry.
Inputs: edges (required), handle (required), radius, per_edge, +2 more.
fit_checkFit CheckARead-only
Classify a hole/shaft pair. hole and shaft are {nominal, plus, minus} (signed deviations) or {nominal, tol}. Returns {fit_class:'clearance'| 'transition'|'interference', min_clearance, max_clearance, nominal_clearance, prob_interference} (prob from a normal model with half-band = 3-sigma).
Inputs: hole (required), shaft (required), workspace.
fit_classFit ClassARead-only
ISO 286 limits for a fit code (e.g. 'H7/g6'), in mm. v1 covers a hole-basis H with shaft clearance letters (h, g, f, e). Returns {basic_size, fit, hole:{upper_dev,lower_dev,min,max}, shaft:{...}, fit_class, min_clearance, max_clearance, prob_interference}. Errors on an out-of-table size (>500 mm) or an unsupported code (non-H hole or interference shaft letter).
Inputs: basic_size (required), fit, workspace.
fit_pageFit Drawing to PageADestructive
Auto-fit a drawing to its sheet: recentre the views so the part AND its
placed dimensions sit inside the printable border (clear of the title block).
The projection group's Automatic scale already sizes the part; its dimensions
extend a fixed margin beyond it which can run off an edge — call this after
placing dimensions to slide everything inside. margin mm is the border inset.
Returns {scale, fits, envelope, border}; fits=False means the part + dims are too
large even when centred (use a larger sheet).
Inputs: page (required), margin, workspace.
fluid_propsFluid PropsARead-only
Thermophysical properties of a fluid at (T, P) from CoolProp's equation of
state (issue #100). name is a fluid (e.g. 'water', 'air', 'R134a', 'CO2',
'nitrogen'), T_K absolute temperature [K], P_Pa pressure [Pa, default
1 atm). Returns {ok, density [kg/m³], viscosity [Pa·s], cp [J/kg·K],
conductivity [W/m·K], prandtl, kinematic_viscosity [m²/s], fidelity,
valid_range_ok, source, warnings, coolprop_available}. This is the DEFAULT
fluid-property source behind the convection/CFD screens; explicit caller props
still override. Degrades cleanly when the (opt-in) CoolProp extra is absent:
air/water return ≈20 °C constants (fidelity='constant_fallback'); other fluids
return {ok:false, reason, install}. CoolProp (BSD-3) is cited as the source.
Inputs: T_K (required), name (required), P_Pa, workspace.
fracture_checkFracture CheckARead-only
Rate brittle fracture (LEFM): K = Y·σ·√(π·a) vs K_IC. a is crack length in mm; Y (geometry_factor) defaults 1.12 (edge crack), 1.0 for a centre crack. K_IC from the material (or override). Critical crack a_c = (K_IC/(Y·σ))²/π. Returns {k_applied_mpa_sqrt_m, k_ic_mpa_sqrt_m, geometry_factor, safety_factor, margin, critical_crack_mm, pass}; a crack past a_c gives SF<1 and margin<0.
Inputs: stress_mpa (required), crack_len_mm (required), material, workspace, +2 more.
fsi_channel_pressureFSI Channel PressureARead-only
Fully-developed plane-channel pressure drop Δp = 12·μ·U·L/h² (NO solver) —
the fluid load that physically sources the pressure-loaded-plate FSI anchor.
For laminar flow between parallel plates a gap gap_mm apart with mean velocity
velocity_m_s over length length_mm, the exact plane-Poiseuille wall pressure
drop feeds fsi_plate_deflection/fsi_interface_balance as pressure_pa. The
Reynolds number Re = ρ·U·h/μ flags when the laminar (exact) assumption holds
(Re ≲ 1400). Default fluid is water at 20 °C (μ=1e-3 Pa·s, ρ=1000).
Returns {pressure_pa, pressure_drop_pa, reynolds, regime, velocity_m_s, wall_shear_pa, fidelity, valid_range_ok, warnings, escalate_to}.
Inputs: gap_mm (required), length_mm (required), velocity_m_s (required), mu_pa_s, +2 more.
fsi_interface_balanceFSI Interface BalanceARead-only
Partitioned wet-interface force balance (NO solver) — the FSI analogue of
the optics energy-balance gate. The fluid presses a uniform pressure_pa over
the length_mm×width_mm strip, so the total traction is F = pressure·area; a
converged partitioned solve must hand the solid exactly this load and the
solid's support reactions must carry it (Newton's third law across the coupling
surface). Pass the measured fluid_force_n (∮p·dA over the OpenFOAM wet patch)
and/or solid_reaction_n (Σ ccx reaction at the clamp); the relative residual
|F_fluid − R_solid|/F_fluid is the conservation error. With neither supplied it
returns the reference analytic load (residual 0) the real solve closes on.
Returns {area_mm2, reference_load_n, fluid_force_n, solid_reaction_n, residual_n, relative_residual, balanced, fidelity, escalate_to}.
Inputs: width_mm (required), length_mm (required), pressure_pa (required), workspace, +2 more.
fsi_plate_deflectionFSI Plate DeflectionARead-only
Exact small-deflection tip/centre deflection of a uniform-pressure-loaded
thin plate strip (NO solver) — the closed-form twin the coupled
OpenFOAM→CalculiX FSI solve (fsi_pressure_plate_submit) is gated against. The
wetted strip is width_mm×length_mm; the fluid pressure pressure_pa (Pa)
acts normal to it, giving the line load q = pressure·width. Section is the
solid rectangle I = width·thickness³/12 unless an explicit i_mm4 is given. E
from youngs_gpa or a Materials-DB material. support='cantilever' (clamped
one edge): δ_tip = q·L⁴/(8·E·I), root moment q·L²/2, reaction q·L; 'clamped-
clamped': centre δ = q·L⁴/(384·E·I), reaction q·L/2 each. Valid while δ ≲
thickness (small-deflection); past that escalate to an NLGEOM follower-pressure
ccx solve.
Returns {support, pressure_pa, line_load_n_per_mm, total_load_n, I_mm4, tip_disp_mm, root_moment_nmm, reaction_n, max_stress_mpa, youngs_mpa, slenderness, fidelity, band_pct, valid_range_ok, warnings, escalate_to}.
Inputs: width_mm (required), length_mm (required), pressure_pa (required), thickness_mm (required), +5 more.
fsi_pressure_plate_submitFSI Pressure Plate SubmitA
Partitioned fluid-structure-interaction solve on the preCICE OpenFOAM↔
CalculiX stack, asynchronous (OFF the MCP channel) — the real coupled-field
twin of the analytic fsi_plate_deflection / fsi_interface_balance oracles.
A flexible flap clamped at a channel floor deflects under the flow: OpenFOAM
(pimpleFoam) writes the wet-interface Force, ccx_preCICE returns the
Displacement, and preCICE drives the implicit coupling to convergence each
time window. preCICE is LGPL-3.0 and the two heavy solvers run ONLY as
subprocesses; degrades to {ok:false, reason, install, stack} when the stack is
absent (build via scripts/install-solvers.sh fsi).
Physics knobs: inlet_velocity_m_s, nu_m2_s, rho_kg_m3 (fluid),
youngs_pa/poisson/density_kg_m3 (solid), end_time_s/time_window_s/
max_iterations (coupling). Geometry/mesh come from the validated vendored
template (no FreeCAD touch). Returns the degradation dict, or {job_id, status,
cache_hit}; poll job_result for {ok, time_windows, tip_disp_m, tip_history,
coupling_converged, case_dir} — the tip displacement is the field the
fsi_plate_deflection oracle gates.
Inputs: nu_m2_s, poisson, timeout, rho_kg_m3, +7 more.
gdt_checkGD&T CheckARead-only
Check a measured feature against a GD&T tolerance zone. control: position | flatness | straightness | circularity | cylindricity | perpendicularity | parallelism | angularity | concentricity | runout | total_runout | profile_line | profile_surface. actual is the measured deviation; for position pass offset={x,y} to use the diametral 2*hypot(x,y). mmc_bonus adds bonus tolerance. Returns {control, zone, effective_zone, actual, margin, pass, datum_refs}.
Inputs: zone (required), control (required), actual, offset, +3 more.
gear_ratingGear RatingARead-only
Rate spur-gear tooth bending (Lewis): sigma=Ft/(bmY). Pass
tangential_force_n, or power_w + pinion_speed_rpm. Returns {tangential_force_n,
pitch_dia_mm, pitch_line_velocity_m_s, lewis_form_factor, bending_stress_mpa,
allowable_bending_mpa, bending_sf, pass}. First-order screen, not full AGMA.
The allowable defaults to the material's fatigue endurance (else 0.3·UTS);
allowable_bending_mpa overrides it with an AGMA/spec number.
Inputs: teeth (required), module_mm (required), face_width_mm (required), power_w, +6 more.
get_interfaceGet InterfaceARead-only
Read back a single published interface FRAME by name from a component (issue #139). The reference-by-name primitive feature templates ride on: an unpublished name fails loudly. Returns {handle, name, frame}.
Inputs: name (required), handle (required), workspace.
get_objectGet ObjectARead-only
Dump a handle's properties + shape stats. Useful when no dedicated tool exposes what you need.
Inputs: handle (required), workspace.
granular_screenGranular ScreenARead-only
Closed-form granular/powder-mechanics oracles — banded correlations, NO
external solver (the FreeCAD-free analytic twins the YADE DEM solve is gated
against). These are correlations, not exact theory, so each returns
fidelity='correlation' + an honest [low, high] band; the band IS the oracle.
Dispatch on problem:
'packing' (regime='random_close'|'random_loose'|'fcc'[, coordination]) — monodisperse sphere solid-volume fraction φ. RCP ≈ 0.637 (band 0.60–0.66), the random pile a real settle must hit, well below the crystalline FCC/HCP 0.7405. 'beverloo' (outlet_m, particle_d_m[, bulk_density_kg_m3 | material]) — flat-bottom hopper discharge W = C·ρ·√g·(D−k·d)^2.5 [kg/s]; flow ∝ outlet to the 2.5 power, independent of fill height. 'beverloo_exponent' (outlet1_m, flow1_kg_s, outlet2_m, flow2_kg_s [, particle_d_m]) — recover the log-log flow exponent from two (outlet, flow) points; granular 2.5 (band 2.2–2.8) vs Torricelli 2.0. 'repose' (friction_coeff[, saturation]) — poured-pile repose angle θ ≈ atan(μ) + ±25% band. 'repose_monotone' (mu_low, repose_low_deg, mu_high, repose_high_deg) — the steeper-with-friction monotonicity gate.
SI units (m, kg/m³, kg/s, degrees). Escalate to dem_pack_submit / dem_flow_submit (the real YADE solve) for polydisperse mixes, non-spherical grains, cohesion, or geometry this monodisperse idealization can't see.
Inputs: mu_low, regime, mu_high, problem, +14 more.
grid_convergenceGrid ConvergenceARead-only
Grid Convergence Index — how much of a solved number is the MESH (no solver, milliseconds). Give it the same quantity solved on 2-3 systematically refined meshes, FINEST FIRST, and it fits the observed order of convergence, Richardson-extrapolates to zero cell size, and returns the percentage band inside which the mesh-independent answer lies. Roache's GCI as codified in ASME V&V 20.
This is the honest band_pct for a result with no analytic oracle — the
verification counterpart to every validation ratio in the solver families — and it
is deliberately family-agnostic: three CFD drag coefficients, three FEM peak
stresses and three modal frequencies are all valid input; only you know what the
mesh size means. cfd_mesh_independence_submit is the driver that produces the
three CFD values for you.
Describe the meshes with either cell_sizes (representative cell length, same
order as values) or cell_counts (total cells; h = N^(-1/dimensions)). With
THREE values the order is measured and the safety factor is 1.25; with TWO it must
be assumed (assumed_order, default 2.0) and the factor triples to 3.0 — a much
wider band, which is the honest price of the missing mesh.
Watch three fields before quoting the band: monotonic False means the solutions
oscillate and the extrapolation is not meaningful (usually an unconverged level,
not a mesh effect); asymptotic_ratio far from 1 means the meshes have not reached
the range where the theory holds, so gci_pct is a LOWER bound; order_clamped
True means the fitted order was unphysical and a clamped one was used.
Returns {n_levels, values, cell_sizes, refinement_ratios, observed_order, order_used, order_clamped, extrapolated_value, gci_pct, gci_coarse_pct, band_pct, relative_error_pct, monotonic, asymptotic_ratio, safety_factor, converged_fit, fidelity, warnings}.
Inputs: values (required), workspace, cell_sizes, dimensions, +2 more.
harmonic_responseHarmonic ResponseARead-only
Exact SDOF harmonic frequency response (NO solver) — the FRF screen and the
oracle the Elmer harmonic_response_submit sweep is gated against. Bridges
beam_modal (f_n) and random_vibration (Q = 1/(2ζ)): with r = f/f_n,
|H| = 1/√((1−r²)²+(2ζr)²), phase = atan2(2ζr, 1−r²), peak amplification
Q = 1/(2ζ√(1−ζ²)) at f_peak = f_n·√(1−2ζ²), half-power bandwidth ≈ 2ζ·f_n.
With frequency_hz the response at that drive is returned;
static_deflection_mm scales it to an absolute amplitude_mm. fidelity='exact';
ζ ≥ 1/√2 has no peak (flagged). Escalate to harmonic_response_submit for a
real meshed FRF (multi-mode, geometry-true).
Returns {natural_frequency_hz, damping_ratio, q_factor, f_peak_hz, half_power_bandwidth_hz, frequency_ratio, amplification, phase_deg, amplitude_mm, fidelity, band_pct, valid_range_ok, warnings, escalate_to}.
Inputs: damping_ratio (required), natural_frequency_hz (required), workspace, frequency_hz, +1 more.
harmonic_response_submitHarmonic Response SubmitADestructive
Harmonic forced response (FRF) via Elmer StressSolve Harmonic Analysis
(SIMULATION_NEXT Tier B2), asynchronous — a plane-stress cantilever driven by
a harmonic tip traction, swept one quasi-static point + n_sweep points across
±span_pct% of its first resonance, with Rayleigh β tuned to damping_ratio
at f₁. Requires ElmerSolver; when absent this returns {ok:false, reason,
install} rather than raising.
Three gates from the in-phase (real) response: f1_ratio — Re(H) = 0 exactly
AT resonance, so the swept tip response's sign-flip locates f₁ vs the
Euler-Bernoulli beam_modal closed form (within ~2%, plane-stress vs beam
theory); static_ratio — the quasi-static point vs the exact tip compliance
F·L³/(3EI) (within ~5%); q_ratio — max|Re|/static vs Q/2 = 1/(4ζ), the
exact SDOF light-damping identity (within ~15%, sweep-sampled). Cross-links
harmonic_response (the SDOF oracle) and random_vibration (same Q). Also
accepts a prepared case_dir.
Returns the degradation dict or {job_id, status, cache_hit}; poll job_result for {ok, f1_solved_hz, f1_eb_hz, f1_ratio, static_solved_m, static_exact_m, static_ratio, peak_over_static, q_factor, q_ratio, frf:[[f_hz, tip_re_m]], case_dir}.
Inputs: nx, ny, sif, n_sweep, +10 more.
helixHelixA
Generate a Part::Helix curve. radius, pitch, height in mm. angle (deg) is the cone angle (0 = cylindrical helix, >0 = conical).
Returns a handle to a 1D helical curve. To get a 3D helical solid (e.g. for threads), use the curve as the spine of a sweep.
Inputs: name, angle, pitch, height, +2 more.
hertz_contactHertz ContactARead-only
Exact Hertzian point-contact peak pressure (NO solver) — the screening twin
of a frictional CONTACT PAIR solve. Sphere of radius_mm on a flat (default)
or on a second sphere radius2_mm (negative for a conforming socket). Each
body's elastics from youngs#_gpa+poisson# or a Materials-DB material#; body 2
defaults to body 1. Reduced modulus 1/E = (1−ν₁²)/E₁ + (1−ν₂²)/E₂, effective
radius 1/R = 1/R₁ + 1/R₂; contact radius a = (3FR/4E*)^(1/3), peak pressure
p₀ = 3F/(2πa²) = 1.5× mean, approach δ = a²/R. Half-space theory: valid while
a ≪ R and p₀ below first sub-surface yield (~1.6·σ_y) — past that escalate to
the nonlinear fem_set_nonlinear_material contact path.
Returns {e_star_mpa, effective_radius_mm, contact_radius_mm, peak_pressure_mpa, mean_pressure_mpa, approach_mm, a_over_R, fidelity, band_pct, valid_range_ok, warnings, escalate_to}.
Inputs: load_n (required), radius_mm (required), poisson1, poisson2, +6 more.
h_estimateHeat Transfer Coefficient EstimateARead-only
Screening convection coefficient h (NO solver) — the honest h_conv to feed
thermal_lumped / thermal_transient_1d / a convection BC, instead of a guess.
geometry picks the correlation: natural (velocity_m_s = 0)
'vertical_plate' | 'horizontal_cylinder' (Churchill–Chu); forced (velocity_m_s > 0)
'flat_plate' (averaged laminar/mixed Nu) | 'cylinder_crossflow' (Hilpert).
characteristic_mm is the plate height/length or cylinder diameter. Film-temp
air properties built in; another fluid needs explicit k_w_mk + nu_m2_s + pr
(+ beta_per_k for natural). emissivity > 0 adds the linearized radiation screen
into h_total_w_m2k.
This is a focusing estimate, not a gate: fidelity='correlation' with band_pct
the literature scatter (±15–20 %). Escalate to the conjugate solve
cht_channel_submit (or a meshed convection BC via thermal_transient_submit)
when the thermal margin is within ~2× band_pct. Returns {geometry, mode,
correlation, h_conv_w_m2k, h_rad_w_m2k, h_total_w_m2k, nusselt, reynolds,
rayleigh, prandtl, film_temp_c, fidelity, band_pct, valid_range_ok, warnings,
escalate_to}.
Inputs: geometry (required), t_surface_c (required), characteristic_mm (required), pr, +8 more.
holeHoleA
Drill a parametric Hole from a sketch (one or more circles) placed on a face of an existing body feature. Lengths mm.
depth_type: 'Dimension' (use depth) or 'ThroughAll'.
cut_type: 'None' | 'Counterbore' | 'Countersink' | 'Counterdrill'; when not
'None', cut_diameter (head clearance) and cut_depth apply.
threaded=True taps it. thread_type / thread_size are COUPLED enums ('M4' fits
'ISOMetricProfile', not 'UNC') — discover them with list_thread_options().
intended_for ('print'|'machine'|'drawing'): sets the ModelThread default when
threaded. print → modelled thread (a printed pilot won't self-tap);
machine/drawing → cosmetic thread (CAM/drawings read the metadata; modelled
threads bloat files and fight patterns/fillets). model_thread overrides it.
through ('wall'|'body'): preferred over depth_type. 'wall' ray-casts to the
first exit and drills exactly one wall — use it on shelled bodies, where
'body' (ThroughAll) destroys the cavity. Implies direction='into_body';
result carries wall_depth_mm.
direction ('into_body'|'away_from_body', preferred over the legacy raw
reversed, honoured only when neither is set; Hole and Pocket read it
differently): picks the Reversed value that does / does not remove material.
strict: raise instead of warn on a degenerate hole.
Returns {handle, name, volume, removed_volume, volume_ratio}, plus warnings
ONLY when the hole consumed the whole body or removed nothing (strict=True
makes those errors). direction='away_from_body' never warns.
Inputs: sketch (required), name, depth, strict, +14 more.
impact_dynamics_submitImpact Dynamics SubmitA
Transient DROP / IMPACT dynamics — the solve drop_impact escalates to. Meshes
body, flies it at velocity_m_s (or the free-fall speed of drop_height_mm —
exactly one) along direction into a fixed rigid floor through penalty contact,
and integrates the motion with CalculiX *DYNAMIC. Async: returns a job — poll
job_result. Degrades to {ok:false, reason, install} when ccx is absent.
direction is the way the part TRAVELS: '-z' (default), '+x', … or any 3-vector —
[-1,-1,-1] is a corner drop (the floor turns, the part is not re-meshed).
Material from material or explicit youngs_mpa/poisson/density_kg_m3
(required, no defaults — the answer scales with √(E·ρ)). yield_mpa arms the
stress criterion; plastic=True (+ tangent_mpa, bilinear) lets it yield
instead. deceleration_limit_g arms the fragility criterion.
method: 'implicit' (default; HHT-α, ccx steps adaptively — right for ms-scale
drops) | 'explicit' (central difference at the stable step — for stress-wave
events ≲ 100 µs; first-order tets). time_step_s on an implicit run switches to a
FIXED step: several times faster where contact comes on smoothly, but ccx stops
if an increment diverges (a flat face landing all at once does). duration_s
defaults to free flight + 10 wave round trips along the drop — enough for a stiff
body; a compliant one needs more, and the gate says so. samples is the single
output cadence ccx allows (force history AND stress frames; default sized to the
mesh). contact: 'auto' (default) | 'face' | 'node' — measured: ccx's face-to-face
penalty never engages a corner strike (the part falls through) and its
node-to-face one locks up on a broad flat landing, so 'auto' uses face contact
down to a 45° edge and node contact for anything sharper. engagement_lag_mm
reports how far the strike point sank before the contact pushed back.
Validated against the exact St-Venant bar (bar_impact): face force ρ·c₀·v₀·A,
contact duration 2L/c₀, restitution 1 and the plastic-wave cap all within ~1–3 %.
A real part is banded (~20 %): peak force depends on the penalty stiffness and
mesh, a stress peak AT the strike point is mesh-dependent, and a sharp wave front
rings ~20 % high. No friction, failure or erosion.
Returns the degradation dict, or {job_id, status, cache_hit}; poll job_result for {ok, returncode, solver, case_dir, nodes, tets, method, direction, velocity_m_s, drop_height_mm, duration_s, time_step_s, mass_g, strike_node, contact_faces, contact, strike_alignment, plastic, bar_stress_mpa, peak_force_n, peak_force_median3_n, peak_force_time_s, peak_g, impulse_n_s, contact_start_s, contact_duration_s, separated, arrested, rebound_velocity_m_s, restitution, energy_end_ratio, energy_min_ratio, mass_check, samples, contact_samples, engagement_lag_mm, peak_von_mises_mpa, peak_stress_node, peak_stress_time_s, peak_stress_location_mm, force_history, gate} where gate is {pass, score, fidelity:"solve", band_pct, checks, utilisation, warnings} — a run that ends before the fall is arrested, or whose energy grows, FAILS.
Inputs: body (required), gap_mm, method, contact, +19 more.
inspection_planInspection PlanARead-only
The ballooned characteristic list for a drawing page as data (every dimension,
feature control frame, feature note), with nominal, limits, and a suggested
measurement method per row. The method follows the tolerance: the ratio:1
gauge rule (default 10:1) walked down a per-family instrument ladder (bores
use pin/bore gauges; datum-referenced GD&T → CMM; datum-free form → surface
plate). Resolution is exact; instrument mapping is shop convention, hence
fidelity='correlation'.
Returns {ok, characteristics, count, by_method, unmeasurable, retired,
next_balloon, fidelity, band_pct, basis}. ok=False means something cannot be
inspected as drawn: no_tolerance (untoleranced size) or no_instrument (band
finer than any instrument), named in unmeasurable.
Inputs: page (required), ratio, workspace.
interface_align_checkInterface Align CheckARead-only
Gate: verify declared interface pairs coincide in world space — the "do the OTHER interfaces line up?" check for multi-interface mates. After the primary mate seats a part, this confirms its secondary interfaces (a second bolt pattern, a bore axis) actually meet the parent's.
pairs: [{child, child_iface, parent, parent_iface}, ...] (child/parent are link names in the assembly). Returns misaligned pairs [{..., gap_mm}], empty if every pair coincides within tol_mm.
Inputs: pairs (required), assembly (required), tol_mm, workspace.
interference_checkInterference CheckARead-only
Pairwise interference: compute volume of intersection between every pair of parts. Returns [{a, b, interference_mm3}, ...] descending by volume. Empty list = no interference.
Inputs: assembly (required), workspace.
items_check_manifestItems Check ManifestARead-only
Reference-integrity guard (#140 C1): check every item-reference in a manifest ({"item":""} on a component/instance) resolves against an items.json registry. Returns {ok, problems} — a dangling item-ref is caught before a merge, removing the "a part is its filename" fragility.
Inputs: manifest (required), registry (required), workspace.
items_newNew ItemA
Allocate a non-significant sequential part number and register a new item in an items.json sidecar (created if absent), writing it back. The reserved rev / lifecycle fields are seeded with held defaults (the #141 state machine, not C1).
registry: path to the items.json sidecar (created if it does not exist). item: the new item's stable logical id (what manifests reference). files: optional list of artifact paths the item maps to. metadata: optional free-form, queryable attributes (where "meaning" lives).
Returns {part_number, item, registry}.
Inputs: item (required), registry (required), files, metadata, +1 more.
items_resolveItems ResolveARead-only
Resolve an item-reference (an item id) to its artifact file(s) against an items.json registry — identity, not a bare path, so renaming/moving a file updates the item's files[] without breaking references. Returns {ok, files}, or {ok:false, problems} on a dangling reference (an unknown item id).
Inputs: item (required), registry (required), workspace.
items_validateItems ValidateARead-only
Validate an items.json registry (the PLM item/document/file split, #140 C1). Checks the schema stamp ("ankusdrive.items/1"), each item record's shape, that part numbers are unique, and the held reserved rev/lifecycle fields. An item is the logical part (part_number + rev + lifecycle + metadata), distinct from its file artifact(s); part numbers are non-significant + sequential, with meaning in queryable metadata.
registry: path to the items.json sidecar.
Returns {ok, problems, schema, count} — ok is True iff problems is empty.
Inputs: registry (required), workspace.
job_listJob ListARead-only
List every async job this worker session. Returns {count, jobs:[{job_id,
kind, status, elapsed_s}]} in submit order, plus main_thread_queue:{queued, ran,
failed, drains, pending} — the diagnostic for a shape optimization that looks
stuck. Those builds run on the worker's main thread, and that queue is drained
once per request: pending high with drains climbing means the work is slow;
drains flat means nothing is polling, so nothing is advancing.
Inputs: workspace.
job_resultJob ResultBRead-only
Fetch an async job's outcome. Returns {job_id, kind, status, elapsed_s, result (when done) | error (when failed)}; while running neither is set. discard=True frees a terminal job (and its cache entry) once you have it.
Inputs: job_id (required), discard, workspace.
job_statusJob StatusARead-only
Lightweight poll of any async job (from an *_submit tool). Returns {job_id, kind, status: 'running'|'done'|'failed', elapsed_s, meta} (+ error when failed) WITHOUT the result payload — cheap to call in a loop.
Inputs: job_id (required), workspace.
journal_exportJournal ExportARead-only
Export a past session from the on-disk journal (off by default; on with
ANKUSDRIVE_JOURNAL_DIR) as session_transcript would have: the replay script, the
recorded environment, and the ordered call ledger with each result's SHA-256.
Read-only. session: an id or "latest"; omit to list sessions. workspace: default
is the one current when the session ended. provenance=False skips solver version
probes. A redacted journal (ANKUSDRIVE_JOURNAL_REDACT) exports an audit record,
not a runnable script. More options: ankusdrive journal export.
Returns {enabled, dir, live_session, sessions} when listing, else {session, workspace, env, script, ledger, warnings, prerequisites, truncated, redacted, integrity, provenance, ...}.
Inputs: session, workspace, provenance.
laminate_propertiesLaminate PropertiesARead-only
Effective stiffness, thermal warp, and first-ply failure of a bonded
multi-layer composite stack (NO solver) — the closed-form screening twin a
layered multi-material CalculiX fem_run static solve is gated against (e.g.
1 metal layer + 1 plastic layer, or any [(material, thickness), …]).
layers is the stack bottom→top; each entry is a mapping with a thickness
(mm) and either a corpus material name or explicit E/youngs_mpa/
youngs_gpa (+ optional nu/poisson, yield_mpa, cte/cte_per_k,
density_kg_m3, thermal_conductivity_w_mk); explicit values override the
card. width_mm scales EI / first-ply. Optional delta_T (K) gives the
bimetal thermal curvature; force_n (in-plane, total across width) and/or
moment_nmm (about the neutral axis) give the first-ply margin.
Computes the in-plane modulus (Voigt rule-of-mixtures parallel, Reuss series
through-thickness); the transformed-section neutral axis, EI_eff, and flexural
modulus E_flex = 12·EI/(b·h³); the CLT A/B/D matrices per unit width (B ≠ 0 ⇒
bending–extension coupling / warp warning); mass-averaged ρ, stiffness-weighted
in-plane CTE, series/parallel thermal conductivity; the transformed-section
bimetal curvature (= Timoshenko's two-layer formula exactly, also reported);
and per-layer extreme-fibre stress → margin to yield → governing layer + load
to first yield. A single-material stack reduces to that material's E / EI; a
symmetric stack gives B = 0; ΔT = 0 or zero CTE-mismatch gives zero curl.
Escalate to a layered fem_run solve for thick stacks, anticlastic curvature,
free-edge interlaminar stress, or non-isotropic plies.
Returns {n_layers, width_mm, total_thickness_mm, layers, E_inplane_mpa, E_through_mpa, neutral_axis_mm, EI_eff_nmm2, E_flex_mpa, A_matrix, B_matrix, D_matrix, coupling_ratio, asymmetric, rho_eff_kg_m3, cte_eff_per_k, k_through_w_mk, k_inplane_w_mk, delta_T, thermal_curvature_per_mm, radius_of_curvature_mm, timoshenko_curvature_per_mm, applied_force_n, applied_moment_nmm, kappa_applied_per_mm, axial_strain, layer_stresses, first_ply, fidelity, band_pct, valid_range_ok, warnings, escalate_to}.
Inputs: layers (required), delta_T, force_n, width_mm, +2 more.
lifecycle_apply_changeLifecycle Apply ChangeADestructive
Apply a change to a RELEASED item, dispatching on the F3 predicate (#141 C2)
— the sanctioned way to change a frozen part. An F3-preserving change opens a
new revision on the SAME part number (rev A->B, back to in_work); an
F3-breaking change allocates a NEW part number as a new item (new_item
required) carrying a supersedes back-link, leaving the released item
untouched.
registry: path to the items.json sidecar (written back on success). item: the released item id being changed. after: the proposed new metadata (attribute object). new_item: id for the new item when the change is F3-breaking. extra_f3 / actor / note: optional, as in lifecycle_classify_change / lifecycle_transition.
Returns {ok, disposition, item, part_number, rev, ...}, or {ok:false, problems} (e.g. the item is not released, or an F3-break lacks new_item).
Inputs: item (required), after (required), registry (required), note, +4 more.
lifecycle_classify_changeLifecycle Classify ChangeARead-only
The deterministic Form/Fit/Function predicate (#141 C2): compare an item's before/after attributes and decide rename-vs-revise. A change touching a Form/Fit/Function (public, interface-defining) attribute breaks interchangeability => "new_part_number" (allocate a new number); a change to only internal/hidden attributes is interchangeable => "revise" (bump the revision, same part number).
before / after: attribute objects (the item's interface-defining + internal attributes, before and after the proposed change). extra_f3: optional map of extra attribute name -> F3 leg ("form"/"fit"/ "function") for domain-specific interface attributes.
Returns the verdict {disposition, f3, changed, f3_changed, categories, reason} where disposition is "revise" / "new_part_number" / "noop".
Inputs: after (required), before (required), extra_f3, workspace.
lifecycle_editableLifecycle EditableARead-only
The cheap "is this editable?" check a builder runs before writing (#141 C2). An item is editable only in lifecycle state in_work; in_review, released and obsolete are frozen (released = immutable, the API-stability guarantee).
registry: path to the items.json sidecar. item: the item id to check.
Returns {ok, editable, state}.
Inputs: item (required), registry (required), workspace.
lifecycle_transitionLifecycle TransitionADestructive
Move an item through the lifecycle state machine in_work -> in_review -> released -> obsolete, guarded by a transition table (#141 C2). An illegal edge (e.g. skipping review, or re-opening a released item in place) is rejected loudly; releasing stamps the item's first revision and freezes it.
registry: path to the items.json sidecar (written back on success). item: the item id to transition. to: the target lifecycle state. actor / note: optional provenance recorded in the item's transition log.
Returns {ok, state, rev}, or {ok:false, problems} on an illegal transition.
Inputs: to (required), item (required), registry (required), note, +2 more.
linear_patternLinear PatternB
Repeat a PartDesign feature linearly along a direction.
direction: 'X'|'Y'|'Z' for body origin axes, or {handle, edge: tag|'EdgeN'} for an edge-aligned direction. length: total span (mm) covered by the pattern. occurrences: number of copies (>=2). Includes the original.
Inputs: feature (required), name, length, reversed, +3 more.
list_assembly_partsList Assembly PartsBRead-only
List parts of an assembly: name, type, linked-target name, position, volume.
Inputs: assembly (required), workspace.
list_documentsList DocumentsBRead-only
List all open documents: [{name, label, file_path, dirty, active, object_count}, ...].
Inputs: workspace.
list_edgesList EdgesBRead-only
List all edges of a shaped object with stable tags + descriptors.
Returns [{tag, index, kind, length, centroid, axis?, radius?}, ...]. Same stability story as list_faces.
Inputs: handle (required), workspace.
list_face_rolesList Face RolesARead-only
Read back the semantic face roles declared on a part (see annotate_face).
Each entry re-resolves its stored tag against the CURRENT geometry, so a drifted or deleted face is reported rather than silently resolving wrong.
Returns a list (sorted by name) of dicts: name (str) the annotation key role (str) inlet | outlet | sealing | wetted | ambient | mating tag (str) the f_* face tag the role is bound to present (bool) whether that tag still resolves on the current shape index (str) 'FaceN' on the current shape (only when present) meta (dict) the verbatim metadata (only when set)
Inputs: handle (required), workspace.
list_facesList FacesARead-only
List all faces of a shaped object with stable tags + geometric descriptors.
Returns [{tag, index, kind, area, centroid, normal?, axis?, radius?}, ...]. Tags survive geometry edits as long as the face's surface kind, area, centroid, and (where meaningful) normal/axis/radius do not change. Use the tag in subsequent calls instead of the FaceN index.
Inputs: handle (required), workspace.
list_objectsList ObjectsARead-only
List objects in the active document. Returns [{name, type, label}, ...].
Inputs: workspace.
list_thread_optionsList Hole Thread OptionsA
Discover the COUPLED ThreadType / ThreadSize enums on the hole tool.
Call with no args to list valid thread_type values. Call with thread_type=... to list the valid thread_size values for that type (the coupling: thread_size='M4' is valid for 'ISOMetricProfile' but not for 'UNC'). Use this BEFORE calling hole(threaded=True, thread_type=..., thread_size=...) to avoid a failed enum-value call.
Returns either {thread_types: [...]} or {thread_type, thread_sizes: [...]}.
Inputs: workspace, thread_type.
list_workspacesList WorkspacesARead-only
List the live workspaces (freecadcmd processes) and the pool limits. Each entry is {name, alive, idle_s, current}. Use to see who is holding a slot before claiming or closing one.
current is the workspace THIS call runs in: the workspace you passed, else
the use_workspace selection. selected is that server-wide selection, which a
pinned call neither follows nor moves.
Returns {current, selected, max, idle_reap_s, workspaces: [...]}.
Inputs: workspace.
loftLoftB
Loft (additively) between two or more sketches.
sketches: ordered list of sketch handles. The first becomes the Profile, the rest become Sections. closed: True connects the last section back to the first (toroidal). ruled: True uses straight ruled surfaces between adjacent sections.
Inputs: sketches (required), name, ruled, closed, +2 more.
make_assemblyMake AssemblyB
Create an App::Part container to hold linked parts. Returns {handle, name}.
Inputs: name, workspace.
make_bodyMake BodyB
Create a PartDesign Body. Subsequent sketches/features go inside it.
Returns {handle, name}.
Inputs: name, workspace.
make_datum_planeMake Datum PlaneA
Create a Datum Plane in a Body.
base: 'XY' | 'XZ' | 'YZ' for body origin planes. Pass a face_tag dict {handle, tag} for attachment to a face on another shape. offset: shift along the plane normal in mm.
Inputs: body (required), base, name, offset, +1 more.
make_drawing_pageMake Drawing PageB
Create a TechDraw page using a built-in A4 landscape template by default. template: optional absolute path to a .svg template.
Inputs: name, template, workspace.
make_sketchMake SketchA
Create a sketch in a Body, attached to a plane.
plane: 'XY' | 'XZ' | 'YZ' for origin planes, or a datum-plane handle. Returns {handle, name}.
Inputs: body (required), name, plane, workspace.
mass_propertiesMass PropertiesARead-only
Mass properties of a shaped object: volume (mm³), surface area (mm²), centroid, bounding box, inertia tensor. If density (kg/mm³) is given, also returns mass (kg). Steel = 7.9e-6, aluminum = 2.7e-6, ABS = 1.05e-6.
Inputs: handle (required), density, workspace.
material_getMaterial GetARead-only
Look up a material by name (e.g. 'AL6061-T6'). Returns the full property card as SI quantity strings (YoungsModulus, PoissonRatio, Density, yield_strength, fracture_toughness, thermal_conductivity, cte, rough_cost, refractive_index where applicable, source, basis). The structural keys are FEM-card-compatible, so the result feeds fem_set_material directly. On a miss returns {ok:false, reason} with a did_you_mean suggestion.
Inputs: name (required), workspace.
material_listMaterial ListARead-only
List available materials, optionally filtered to one category ('aluminum' | 'steel' | 'titanium' | 'magnesium' | 'polymer' | 'glass'). Returns {count, category, materials:[{name, category}, ...]} sorted by name.
Inputs: category, workspace.
material_selectMaterial SelectARead-only
Ashby-style selection: filter the corpus, then rank survivors.
criteria keys are min_/max_ (e.g. min_yield_mpa, max_density_g_cc, min_service_temp_c). rank_by: specific_strength | specific_stiffness | strength | stiffness | cost | density. Returns {rank_by, count, criteria, candidates:[{name, score, yield_mpa, density_g_cc, youngs_gpa, cost_usd_kg}, ...]} best-first; an empty filter returns no candidates rather than the closest miss.
Inputs: rank_by, criteria, workspace.
measure_angleMeasure AngleARead-only
Angle (degrees) between two planar faces or two straight edges.
a, b: object handles. a_ref, b_ref: REQUIRED sub-shape references, one per handle. Both must be the SAME kind:
face tags ('f_*' from list_faces/query_faces, or 'FaceN', or 1-based int) -> angle is between the faces' outward normals. Faces must be planar.
edge tags ('e_*' from list_edges, or 'EdgeN', or 1-based int) -> angle is between the edges' tangent directions. Edges must be straight. Mixing a face ref with an edge ref, a non-planar face, or a curved edge raises.
Units: degrees. Returns:
angle_deg: raw angle between the two direction vectors, 0..180.
supplement_deg: 180 - angle_deg (the complementary angle; use this for the acute reading when angle_deg is obtuse).
kind: "face" or "edge". Two adjacent box faces -> angle_deg 90. Two opposite parallel box faces -> angle_deg 180, supplement_deg 0. Read-only: measures, creates no geometry.
Inputs: a (required), b (required), a_ref (required), b_ref (required), +1 more.
measure_distanceMeasure DistanceARead-only
Minimum (closest-approach) distance between two entities, mm — verify gaps, clearances, and contact. 0 means touching or interpenetrating; for overlap volume use min_clearance / interference_check.
a, b: object handles. a_ref, b_ref: optional sub-shape to measure from instead of the whole solid — an f_* / e_* tag or "FaceN"/"EdgeN" (1-based).
Returns {distance_mm, point_on_a ([x,y,z] mm), point_on_b, touching (distance_mm < 1e-7)}. A measurement: no handle.
Inputs: a (required), b (required), a_ref, b_ref, +1 more.
mechanism_kinematicsMechanism KinematicsARead-only
Closed-form planar mechanism kinematics — exact, NO external solver (the static
pre-check / gate for mechanism_simulate_submit). Pick mechanism:
'fourbar': link lengths crank/coupler/rocker/ground -> {mobility_dof (=1), grashof: {condition, type, input_crank_fully_rotates, shortest}, reachable, n_reached, coupler_path [[x,y]...], reachable_bbox_mm}.
config'open'|'crossed'.'slider_crank': crank_mm/conrod_mm (+ wrist_offset_mm) -> {stroke_mm (exactly 2·R in-line, independent of conrod), x_tdc_mm, x_bdc_mm, inline_stroke_exact}.
'gruebler': n_links (incl. ground) + joints ([{type}...]) -> {mobility_dof}.
Returns the per-mechanism dict above. Raises on an unknown mechanism or a link set that cannot close.
Inputs: crank, config, ground, joints, +10 more.
mechanism_simulate_submitMechanism Simulate SubmitA
Simulate a rigid-link mechanism's DYNAMICS with PyBullet, asynchronously (the
MBD family; requires the mbd extra — pip install 'ankusdrive[mbd]'). Use
mechanism_kinematics first for the exact closed-form gates (DOF, Grashof, stroke).
links is a tree: [{name, box_mm:[lx,ly,lz], mass_g, parent (link index, −1 =
fixed base), joint_type ('revolute'|'prismatic'|'fixed'), joint_axis:[x,y,z],
joint_at_mm:[x,y,z] (in the parent frame), com_mm:[x,y,z]}]. drivers:
[{link, rate_dps}] (revolute) or [{link, rate_mm_s}] (prismatic). Optional
obstacles ([{box_mm, at_mm}]) for through-motion contact, base, gravity
(m/s², default [0,0,−9.81]), dt_s, duration_s. gears
([{link_a, link_b, ratio, axis?, max_force?}]) couples two revolute links by
ω_b = −ω_a/ratio (ratio = Nb/Na for an Na/Nb external mesh) — the moving image of
the gear-train ratio gate.
Returns immediately. If PyBullet is absent: {ok:false, reason, install, mobility_dof, n_links}. Otherwise {job_id, status, cache_hit, mobility_dof}; poll job_result(job_id) for {trajectories, orientations (per-link world quaternion, sampled with trajectories), max_torques, collisions_through_motion (with the sim time of each contact), reachable_envelope {bbox_mm}, mobility_dof}.
Inputs: links (required), base, dt_s, gears, +6 more.
merge_assemblyMerge AssemblyA
Construct-up an assembly from a manifest JSON (the coordinator's one call): create the doc, link each component by file path, place it, recompute, and run the gates. Component files resolve relative to the manifest's directory; links auto-reload, so re-running picks up updated components (deterministic, idempotent).
manifest shape: { "name": "gearbox", "root": "gearbox.FCStd", "components": {"": {"file": "rel/part.FCStd", "object": ""?, "envelope": {"min":[...],"max":[...]}?}}, "instances": [{"component":"", "name":""?, "placement": [x,y,z] | {position,axis,angle_deg}, "mate": {"child_iface","parent","parent_iface", "verify_align":{"child_iface","parent_iface"}?}?}], "mates": [{"child","parent","child_iface","parent_iface", "verify_align":{...}?}]? }
Placement positions anchors; mate-by-frame positions everything else by aligning published interface frames (see publish_interface).
Any component carrying a PERFORMANCE contract (declare_performance, #226) is gated on it too, with no manifest opt-in: the merge consults the verdict verify_performance last RECORDED on that part and never measures, so it stays synchronous and deterministic. A requirement measured as NOT met fails the merge; a requirement with no verdict yet is neither passed nor failed and rides in report["performance"]["skipped"] — "unverified" is never read as "fine".
Returns {assembly, doc, root, placed, gates:{interference, bom, envelope,
interface_align?, typed?, requirements?, mobility?, performance?}, ok,
requirements?, mobility?, performance?, children?, library?}. The performance
gate and report block are absent entirely when no component declares a contract.
Inputs: manifest (required), workspace.
min_clearanceMin ClearanceARead-only
Closest approach between two solids — the measured gap, richer than the
binary interference_check. a and b are object handles. All lengths mm,
volumes mm³.
Returns a dict:
status: "clear" (a positive gap separates them),
"contact" (faces/edges touch, gap ~ 0), or
"interference" (the solids interpenetrate / share material).
clearance_mm: minimum distance between the two solids (mm). 0.0 when they
are touching or interfering.
overlap_volume_mm3: volume of interpenetration (mm³). Present ONLY when
status == "interference".
point_on_a: [x,y,z] of the closest point on a. Present when status is
"clear" or "contact" (omitted for "interference").
point_on_b: [x,y,z] of the closest point on b. Present when status is
"clear" or "contact" (omitted for "interference").
Inputs: a (required), b (required), workspace.
mirroredMirroredB
Mirror a PartDesign feature across a plane.
plane: 'XY'|'XZ'|'YZ' for body origin planes, a datum-plane handle string, or {handle, face: tag|'FaceN'} for a face-defined mirror plane.
Inputs: feature (required), name, plane, workspace.
moldability_checkMoldability CheckARead-only
Geometry-aware moldability DFx screen — resolves the model handle's
solid, samples local wall thickness per face via inward chords (the same
machinery as optics_moldability_check), then grades it through the pure-Python
moldability screen: WALL-THICKNESS QUALITY (recommended-band range /
uniformity / sink risk, cooling tied to the thickest wall) + the CTE
SHRINKAGE estimate for the resin. Low-fidelity gate — escalate_to=
'molding_fill_submit'.
material drives the recommended-wall band, the CTE shrinkage, and cooling
(degrades gracefully when the corpus lacks the issue #106 fields).
nominal_mm anchors the range check (else the sampled-wall mean). The same
shrinkage/thickness overrides as moldability_screen apply.
Returns the moldability_screen verdict {thickness:{…}, shrinkage:{…}, pass, score, fidelity, band_pct, warnings, escalate_to} plus {n_faces, n_wall_samples}.
Inputs: model (required), material, workspace, fail_ratio, +6 more.
moldability_screenMoldability ScreenARead-only
Moldability DFx screen (NO solver, NO geometry) — fast analytic gate combining two checks molders reason about first: (1) WALL-THICKNESS QUALITY — is the nominal wall in the resin's recommended moldable band, and is the section uniform enough (uniformity_ratio = t_max/t_min; warn >2, fail >3) to avoid sink/warp; thick-lobe samples (> sink_factor·nominal, k≈1.5) flagged; cooling tied to the thickest wall (t ∝ s²). (2) SHRINKAGE — first-order from the resin CTE: S_linear = alpha·ΔT, S_vol ≈ 3·S_linear, cavity_scale_factor = 1/(1−S_linear); semicrystalline resins (PP/PE/PA/POM/PLA/HDPE/LDPE) flag model_underpredicts and carry a published_shrinkage_pct.
Pass wall_samples (local wall thicknesses, mm) and/or nominal_mm, plus
material. Degrades gracefully when the corpus lacks the (issue #106)
recommended-wall / mold-shrinkage / crystallinity fields. Low-fidelity gate:
escalate_to='molding_fill_submit'.
Returns {thickness:{…}, shrinkage:{…}, material, pass, score, fidelity, band_pct, warnings, escalate_to}.
Inputs: material, workspace, fail_ratio, nominal_mm, +6 more.
molding_fill_submitMolding Fill SubmitADestructive
Injection-molding FILL (+ optional PACK/COOL) solve, asynchronous — the higher-
fidelity twin
molding_screen escalates to. Answers can this geometry actually be molded:
short-shot / fill ability (the strongest, most reliable gate), fill time, and a
peak injection-pressure proxy — a real two-phase (melt + air) flow solve, not the
spiral-flow correlation. Requires an OpenFOAM binary; when none resolves this
returns {ok:false, reason, install} rather than raising.
Backend: when the openInjMoldSim binary resolves (GPL-3.0, a modified
compressibleInterFoam on OpenFOAM-7 .org — Cross-WLF + 2-domain Tait), the worker
generates and runs an OF7-org case from the params below (or runs a prepared
case_dir if given) — a pressure-driven, non-isothermal plaque fill with the
Cross-WLF/Tait coefficients pulled from the materials corpus for resin. Where
that build is absent it falls back to a 2-D plaque-cavity interFoam VOF case on
the existing OpenFOAM (.com/ESI) — same physics family, answers fill/short-shot but
not packing. The GPL solver is held at the subprocess boundary (never imported).
openInjMoldSim (OF7) params: resin (corpus key, default "PS"), length_mm,
wall_thickness_mm (gap), depth_mm, nx/ny, peak_pressure_mpa (gate ramp,
default 2), melt_temp_c (220), mold_temp_c (60), wall_h_w_m2k (wall heat-
transfer coeff; default ~adiabatic for a clean fill — raise for freeze-off),
fill_end (terminate fraction, default 0.98). Pass application to force the
interFoam-prepared path.
Packing/cooling (openInjMoldSim only): set stages="fill_pack" to also run the
cooling continuation after fill (seal the gate, switch walls to cooling, hold). Knobs:
pack_phases (default 2), cool_window_s (cooling duration; default a few× the fill
time — note a 1 mm wall cools in ~seconds, so a short window gives partial cooling),
eject_temp_c (for cooling-time; default 80), pack_wall_h_w_m2k (cool-side wall
coeff; default 1250). The pack result adds pack:{rho_mean_final, rho_min, volumetric_shrinkage_pct, frozen_fraction, cooling_time_s, residual_pressure_pa} and
pack_gate:{pass (on sink risk), score, volumetric_shrinkage_pct, expected_densification_pct (Tait-EOS), pvt_faithful, sink_risk, warnings}.
Net mold shrinkage (the cavity-sizing number; issue #116) — distinct from
pack_gate's raw-PVT densification. The fill_pack result also models the packing-feed
make-up (melt fed at the hold pressure until the gate freezes at the Tait no-flow
transition; only the uncompensated post-gate-freeze densification is net shrinkage)
and gates it against the resin's published linear band. hold_pressure_pa (effective
cavity packing pressure; default = the ramp peak), room_temp_c (free-part relax
temperature; default 23). Adds net_shrinkage:{net_linear_pct, net_vol_pct, raw_vol_pct, compensated_vol_pct, gate_freeze_temp_k} and shrinkage_gate:{pass (in corpus band), score, net_linear_pct, corpus_band_pct, in_band, warnings}. Plus
cooling_dT_through_k — the antisymmetric through-thickness differential auto-derived
from the cooling field, ready to flow straight into molding_warpage_submit (pass it,
or pass this result's case_dir+nx/ny as cooling_case_dir etc.).
Asymmetric per-wall cooling (#134): set pack_wall_h_low_w_m2k and
pack_wall_h_high_w_m2k (the y=0 and y=H mold-face heat-transfer coeffs) to DIFFERENT
values to model an asymmetric cool — the case is meshed with split wallLow/wallHigh
patches and each face cools at its own rate, freezing a real through-thickness bending
differential. This is what makes the live cooling_dT_through_k nonzero (a symmetric
cool correctly gives ≈0 → no warp). The part bows toward the slower-cooled (lower-h,
hotter, last-to-freeze) face; the result adds asymmetric_cooling:{pack_wall_h_low_w_m2k, pack_wall_h_high_w_m2k, warps_toward}. Leave both unset (or equal) for the default
symmetric cool. Feed the resulting case_dir into molding_warpage_submit to predict
the warp magnitude.
Drive the interFoam path with cavity + process params:
length_mm(flow length, default 100),wall_thickness_mm(cavity height, default 2),depth_mm(out-of-plane, default 1), meshnx/ny.inject_velocity_m_sORflow_rate_cm3_s(+ optionalgate_height_mmfor the gate area) — the melt mean inlet speed.melt rheology:
melt_rho_kg_m3(default 900),melt_nu_m2_s(kinematic, default 1e-3) for Newtonian, or acarreau{nu0,nuInf,k,n} BirdCarreau dict (the shear-thinning Cross-WLF stand-in).end_time_s(run bound; default ≈4× the plug-flow fill time),machine_max_pressure_pa(press limit, default 180 MPa),fill_fraction_pass(full-fill threshold, default 0.97). Or pass a preparedcase_dirto run the resolved solver directly.
Returns the degradation dict, or {job_id, status, cache_hit}; poll job_result. interFoam result: {ok, returncode, backend, case_dir, expected_fill_time_s, flow_length_ratio, fill:{filled_fraction, filled_cell_fraction, front_x_frac, last_to_fill_x_frac, max_pressure_pa}, gate:{pass, score, fidelity:"solve", band_pct, short_shot, fill_time_s, max_pressure_pa, pressure_ok, warnings}}.
Inputs: nx, ny, resin, stages, +28 more.
molding_screenMolding ScreenARead-only
Injection-molding screen (NO solver): one-term cooling time (exact given α,
t_cool = s²/(π²α)·ln(8·(T_melt−T_mold)/(π²·(T_eject−T_mold))) — the t ∝ s²
design lever) + the spiral-flow fill check (fill_ok when flow_length ≤
(L/t-limit)·wall — chart correlation, ±30 %). material picks per-polymer
defaults (ABS | PP | PC | PA66 | POM | HDPE | PS), each individually
overridable; with all temps + alpha_mm2_s explicit no material is needed.
A cooling-only call returns fidelity='exact'; adding flow_length_mm makes the
headline answer fidelity='correlation', band_pct=30 (cooling stays exact).
When a flow_length_mm is given the headline check is a chart correlation, so
escalate_to='molding_fill_submit' (the openInjMoldSim VOF fill solve, #105);
a cooling-only call needs no solver and returns escalate_to=None.
Returns {material, wall_thickness_mm, t_melt_c, t_mold_c, t_eject_c, alpha_mm2_s, cooling_time_s, flow_length_mm, flow_ratio, flow_ratio_limit, fill_ok, fidelity, band_pct, valid_range_ok, warnings, escalate_to}.
Inputs: wall_thickness_mm (required), material, t_melt_c, t_mold_c, +5 more.
molding_warpage_submitMolding Warpage SubmitADestructive
Injection-molding WARPAGE / residual distortion, asynchronous — the FEM
thermo-elastic post-step of the cooling solve (GitHub issue #113 Part B; the
higher-fidelity twin of the #104 CTE shrinkage screen). Answers will the part bow
out of flat once it cools and is ejected. Requires the ccx (CalculiX) binary;
when none resolves this returns {ok:false, reason, install} rather than raising.
The physics: a moulding shrinks as it cools (CTE); uniform shrinkage just makes
it smaller, but differential shrinkage warps it. The dominant driver is the
asymmetric frozen-in through-thickness temperature field at ejection (an unbalanced
cooling layout, one mould half hotter, a rib on one face). Pass that as
dT_through_k (K, the through-thickness differential: T at the thin-face minimum
minus the maximum). The worker meshes body, imposes the field as a thermal
eigenstrain, pins a statically-determinate 3-2-1 constraint, and solves the free
linear-elastic distortion in ccx. A balanced field (dT_through_k≈0) warps ~0; an
asymmetric one bows to the analytic plate curvature (κ=α·ΔT/h), directionally
correct.
Backend: CalculiX (ccx), driven by a deck the worker writes directly (the GPL
solver is held at the subprocess boundary, never imported). Units mm / MPa / 1/K /
°C, so warp comes back in mm.
Coupled cooling hand-off (issue #116): instead of hand-passing dT_through_k,
pass cooling_case_dir (a molding_fill_submit(stages="fill_pack") result's
case_dir) with cooling_nx/cooling_ny (the cooling case mesh; defaults 60/8) and
optional cooling_nz/cooling_time — the worker reads that cooling solve's cell-
centre temperature field and auto-derives the antisymmetric (bending) through-
thickness differential. The result reports dT_through_k and dT_source.
Params: body (a shape handle — the part), and either dT_through_k or
cooling_case_dir (one is required). Material
elastic props from material (corpus card) or explicit youngs_mpa/poisson/
cte_per_k — solidified-resin defaults are used with a warning otherwise (the
corpus rheology cards don't carry structural props). ref_temp_c is the stress-
free / solidification temperature (warp is invariant to it — it only scales the
reported residual stress). char_length_mm sets the mesh size; thickness_axis
('x'|'y'|'z') overrides the auto-detected through-thickness axis; flatness_tol_mm
or flatness_tol_frac (default 0.2 % of span) set the gate tolerance.
Fidelity caveat: a one-way, linear-elastic, loose coupling — it ignores
viscoelastic stress relaxation, flow-induced anisotropy, and the packing-pressure
residual; it captures the dominant differential-shrinkage warp and its direction,
not a calibrated absolute. fidelity="solve" with a conservative band_pct; read
the band.
Returns the degradation dict, or {job_id, status, cache_hit}; poll job_result for {ok, returncode, solver, case_dir, nodes, tets, warp_axis, span_mm, thickness_mm, dT_through_k, dT_source, analytic_bow_mm, gate} where gate is {pass (on flatness), score, fidelity:"solve", band_pct, max_warp_mm, flatness_tol_mm, warp_per_span, max_disp_mm, warp_faithful, analytic_bow_mm, warnings}.
Inputs: body (required), poisson, material, cte_per_k, +13 more.
monopole_sphereMonopole SphereARead-only
Exact pulsating (monopole) sphere radiated power + far-field pressure (NO
solver) — the closed-form twin the Bempp exterior-acoustics BEM radiation solve
(acoustic_radiation_submit) is gated against. A sphere of radius a_m
vibrating with uniform surface normal velocity u_amp at freq_hz radiates
W = (ρc/2)|U|²(4πa²)(ka)²/(1+(ka)²) and, at range r_m, |p(r)| =
ρc|U|·ka/√(1+(ka)²)·(a/r) — both EXACT. The radiation efficiency
σ=(ka)²/(1+(ka)²) → 0 (poor sub-wavelength radiator) as ka→0 and → 1 as ka→∞.
rho/c default to air at 20 °C. A BEM Neumann (velocity) solve must reproduce
W and |p(r)|.
Returns {a_m, freq_hz, k_per_m, ka, u_amp, rho, c, radiation_efficiency, radiated_power_w, surface_pressure_abs, r_m, farfield_pressure_abs, farfield_pressure_x_r, fidelity, band_pct, valid_range_ok, warnings, escalate_to}.
Inputs: a_m (required), freq_hz (required), c, r_m, +3 more.
new_documentNew DocumentB
Create a new FreeCAD document and make it active. Returns {doc: }.
Inputs: name, workspace.
open_documentOpen DocumentA
Open an existing .FCStd file, make it active. Returns doc name and object list.
Inputs: path (required), workspace.
optics_lens_designOptics Lens DesignARead-only
First-order + spot analysis of a SEQUENTIAL optical system with optiland
(MIT, in-process). Requires the optics extra; degrades to {ok:false, reason,
install} otherwise. For a single lens optiland is gated against the analytic
thick-lens oracle (oracle_dev_pct).
surfaces: list (object->image) of {radius, thickness, material, stop?} — exactly
one surface must set stop:true (the aperture stop). epd (entrance-pupil dia)
OR fno. wavelengths_um (first is primary, default [0.5876]). field_angles_deg
(default [0.0]). image_solve solves the last gap to paraxial focus.
Returns the degradation dict, or {ok, backend:'optiland', optiland_version, efl_mm, bfl_mm, fno, n_surfaces, rms_spot_um:[per field], oracle_efl_mm, oracle_dev_pct}.
Inputs: surfaces (required), epd, fno, want_spot, +4 more.
optics_lens_optimizeOptics Lens OptimizeARead-only
Optimize a SEQUENTIAL optical system with optiland's optimizer (MIT,
in-process) — the capability rayoptics lacks. Requires the optics extra;
degrades to {ok:false, reason, install} otherwise.
surfaces: as in optics_lens_design. variables: [{type:'radius'|'thickness',
surface:<1-based int>}] — the degrees of freedom. targets: [{operand:'f2'|
'rms_spot_size'|…, target, weight?, surface?}] — the merit function. maxiter
caps iterations.
Returns the degradation dict, or {ok, backend:'optiland', converged, n_fev, before:{efl_mm,rss}, after:{efl_mm,rss}, surfaces:[optimized]}.
Inputs: targets (required), surfaces (required), variables (required), epd, +4 more.
optics_moldability_checkOptics Moldability CheckARead-only
Moldability screen for a part against a single pull axis — geometric, no
solver. Resolves the model handle's solid, then per face computes the draft
relative to pull_axis from the outward normal (draft_deg = 90 − angle(normal,
pull); 0 = a wall parallel to the pull that needs draft) and ray-casts the face
centroid along ±pull: a face the straight pull frees in neither direction is a
re-entrant UNDERCUT (reported with negative draft). Inward chords give a wall-
thickness distribution. Scored through the same DfM machinery as dfm_check.
pull_axis: '+z'/'-x'/… or an [x,y,z] vector. process ('injection'|'cnc'|
'sheet'|'fdm') sets the default min wall; override with min_wall_mm.
Returns {process, pull_axis, n_faces, undercut_faces, draft_violations, min_wall_violations, wall_thickness_stats:{min_mm,mean_mm,max_mm,n}, score, pass}.
Inputs: model (required), process, pull_axis, workspace, +2 more.
optics_raytraceOptics RaytraceARead-only
Ray-trace a bundle through a dielectric optical model with rayoptics
(asynchronous-free; needs no FreeCAD geometry). Requires the rayoptics wheel
(the optics extra); when it does not resolve this returns {ok:false, reason,
install} rather than raising. The geometric refraction comes from rayoptics;
the Fresnel/TIR energy split + the exit histogram come from the exact
analysis/optics core, so the trace is gated against that oracle
(oracle_max_dev_deg = max rayoptics−Snell exit-angle deviation, ~0).
n_refractive is the medium index n2 (default PMMA 1.49062). source_config
is {kind:'collimated'(angle_deg)|'cone'(half_angle_deg)|'lambertian'
(max_angle_deg)} (default collimated at normal incidence). model may carry
{n1 (incident index, default air 1.0), absorption (0..1 bulk loss),
target_half_angle_deg (the acceptance cone counted as efficiency)}.
Returns the degradation dict, or {ok, backend:'rayoptics', rayoptics_version, n_rays, n1, n2, critical_angle_deg, efficiency, leakage_fraction, absorbed_fraction, tir_fraction, energy_balance, oracle_max_dev_deg, exit_distribution:[{angle_deg,intensity}], hotspot_locations}.
Inputs: model, n_rays, workspace, n_refractive, +1 more.
optics_solid_traceOptics Solid TraceARead-only
NON-SEQUENTIAL ray trace through a real solid (STL mesh) with a refractive
index — the lane for molded optical parts (light-pipes, prisms, lenses). Backed
by KrakenOS, which is GPL-3.0 and is run ONLY in a subprocess (the parent never
imports it — same arm's-length isolation as the GPL Elmer/OpenFOAM binaries).
Requires the optics_gpl extra; degrades to {ok:false, reason, install} otherwise.
Geometry: pass a model handle (exported to STL here) OR a ready stl_path.
Material: glass (KrakenOS catalog name, e.g. 'BK7') or n_refractive (constant
index). rays: [{origin:[x,y,z], dir:[l,m,n]}]; each ray's turn_deg is its
input->exit bend (~90 for a TIR corner prism, ~0 for a straight pass). solid:
{diameter, thickness, axis_move} placement. wavelength_um default 0.55.
want_paths (default False): also return each valid ray's polyline as
paths — the per-surface hit points [[x,y,z], ...] in the traced frame.
The LAST point of each path is the ray's EXIT LOCATION on the solid, so the
spatial exit/leakage map a diffuser needs can be reconstructed from it.
Returns the degradation dict, or {ok, backend:'KrakenOS (subprocess-isolated, GPL-3.0)', n_launched, n_valid, valid_fraction, mean_turn_deg, max_turn_deg, rays:[{valid, exit_dir, turn_deg}], stl_path, paths?:[[[x,y,z],...],...]}.
Inputs: rays (required), glass, model, solid, +5 more.
optimize_submitOptimize SubmitA
Vary parameters until the spec is met, then say whether it was PROVEN — the step that closes the design-to-spec loop.
Everything else in this family measures; this searches. Two things make it different from a generic minimizer, and both come from the performance-contract layer underneath it:
A constraint verdict has three states. indeterminate — the measurement's
uncertainty band straddles the limit — is NOT a failed step. An optimizer that reads
it as a failure walks away from good designs; one that reads it as a pass converges
on unproven ones. During the search an indeterminate constraint is scored on its
nominal value, so it neither attracts nor repels, and the winner is proved properly
at the end.
Convergence is not proof. A simplex can settle on a point that clears its limit
by 2 % while its own grid-convergence band is 5 % wide — noise with a favourable
sign. proven is therefore reported separately from converged, and is True only
when the final measurement has every constraint at pass and no margin swallowed by
its own band.
variables must be continuous and bounded — {"name": "diameter_mm", "min": 5, "max": 25, "start": 10}. An optimizer without a box walks to values that satisfy the
arithmetic and mean nothing physically.
objective and each constraints entry use the performance-requirement mapping
({tool, metric, conditions, limit, screen, band_pct}), with "$<variable>" in
conditions carrying the candidate's value. tier='auto' searches cheaply on each
block's screen estimator, then polishes on the real tool from where the screen
landed; 'screen' or 'solver' runs just that leg.
The search is a bounded Nelder-Mead — derivative-free because there is no adjoint
through a CFD solve — so every evaluation is a real measurement. budget
({max_evals, max_wall_s}, default 40 evaluations) is the ceiling; revisited points
are served from cache and do NOT count against it.
Shape optimization. Pass a recipe (+ fixed_inputs) and it is rebuilt for
every candidate, so the search varies GEOMETRY rather than only numbers —
"$handle" in a response's conditions is that candidate's part, and the returned
history carries the handle each point built. Pass handle instead to optimize
parameters against one fixed part; pass neither and the search is purely parametric.
Geometry-driven candidates are built on the MAIN thread through the worker's
work queue, because FreeCAD's document API is not thread-safe and this search runs
as a background job. That queue is drained once per incoming request, so a shape
search only advances while you are polling job_status/job_result — the poll
you must do anyway is what gives it its turn. Poll at your normal cadence and it
simply works; stop polling and it stalls rather than finishing in the background.
A candidate whose recipe fails to build is scored out as an infeasible point, not
an error. study_submit remains the right tool for a FIXED grid over recipe
geometry, which needs no queue at all.
Returns {job_id, status}; poll job_result for {ok, proven, stop_reason, best_params, best_value, objective: {name, metric, sense, value, band_pct}, constraints: [{name, state, measured, limit, band_pct, margin, margin_pct, detail, trust_reasons?}], phases: [{tier, n_evals, best_params, best_value, converged, reason}], history: [{i, tier, params, value, score, feasible, cached}], n_evals, n_cached, budget, variables, warnings}.
Inputs: objective (required), variables (required), tier, budget, +5 more.
oring_grooveO-Ring GrooveA
Compute a static O-ring gland (groove) and optionally cut it into a face.
This is the gland calc designers always fumble, plus an optional cut. Given the O-ring cross-section it returns standard static-seal gland dimensions; with cut=True it also machines the annular groove into a flat face.
cross_section: O-ring wire cross-section diameter in mm (e.g. 1.78, 2.62). Required, > 0. inner_diameter: groove inner diameter in mm (the O-ring's nominal seal ID). Required when cut=True; used to size the returned diameters either way. handle: host solid to cut into (required only when cut=True). face: the flat face to cut the groove into — a stable f_* tag (preferred), a 'FaceN' index string, or an int. Required when cut=True. Must be planar. gland_type: seal-geometry label, default 'static_radial' (informational). compound: optional elastomer + durometer the RING is ordered in ("NBR70", "FKM75", "EPDM70"). Changes no geometry; it completes the AS568 designation of the ring itself — the purchased part this groove exists to hold, which no BOM would otherwise contain because the ring is never a modelled object. cut: True (default) cuts the groove and returns a new solid; False makes this a pure calculator (no geometry, no handle). name: name for the resulting solid when cut=True.
Gland rule (static seal): groove_depth = cross_section0.75 (~25% squeeze, clamped to a 20-30% band), groove_width = cross_section1.30. The groove's inner diameter equals inner_diameter and it spans outward by groove_width.
Returns {groove_depth, groove_width, groove_inner_diameter,
groove_outer_diameter, squeeze_pct, cross_section, gland_type, oring} (all mm
except squeeze_pct in percent). When cut=True it ALSO returns {handle, name,
volume} for the grooved solid; the host input is hidden. mating numbers: cut a
groove of inner_diameter to seat an O-ring of that ID; groove_outer_diameter
sizes the radial space the groove occupies. oring is the RING's designation
card ("AS568-214 NBR70"), or ok=False naming the nearest tabulated sizes when
the gland is not an AS568 standard size — an off-table ring is a custom tooled
part, and saying so beats naming a dash number that will not seal.
oring_catalog is the ring's off-the-shelf verdict.
Inputs: cross_section (required), cut, face, name, +5 more.
pack_checkMolding Pack CheckARead-only
Check a part against a shipping carton + compute billable weight.
part_bbox_mm/carton_mm are [l,w,h] mm; fits allows reorientation (sorted-dim
compare). void_fraction = 1−vol(part)/vol(carton); dim_weight_kg =
vol(carton cm³)/dim_factor (default 5000 metric DIM); billable_weight_kg =
max(actual, dimensional). Returns {fits, void_fraction, dim_weight_kg,
actual_mass_kg, billable_weight_kg, pass}.
Inputs: mass_g (required), carton_mm (required), part_bbox_mm (required), workspace, +1 more.
padPadB
Pad a sketch by length mm. symmetric=True extrudes both directions.
Inputs: sketch (required), name, length, reversed, +2 more.
partdesign_chamferPartDesign ChamferA
PartDesign Chamfer on edges of a feature in a Body. edges accepts e_*
tags or 'EdgeN' index strings; size is the chamfer leg in mm (> 0).
Validated and rolled back on failure exactly like partdesign_fillet (issue #283), with the same per_edge / allow_partial opt-ins.
Returns {handle, name, volume (mm^3), edges (the 'EdgeN' names actually chamfered), checks {valid, solids, envelope_ok, envelope_growth_mm, envelope_tol_mm}, mode, partial}; when partial is True, also skipped_edges and a warnings entry. On a failed check the feature is removed, the Body's Tip is restored, and BlendCheckFailed is raised naming the offending edges.
Inputs: edges (required), feature (required), name, size, +3 more.
partdesign_filletPartDesign FilletA
PartDesign Fillet on edges of a feature in a Body. edges accepts e_*
tags or 'EdgeN' index strings; radius in mm (> 0).
Validated before a handle is issued, like fillet_edges (issue #283): Shape.isValid(), unchanged solid count, and no growth of the tight bounding box. PartDesign needs it as badly as the Part workbench — r=0.6 on a 40x40x1 pad returns an 'Up-to-date' single solid 15% LARGER than the pad.
per_edge: add the edges one at a time, validating after each. allow_partial: accept a partial result instead of aborting. Off by default.
Returns {handle, name, volume (mm^3), edges (the 'EdgeN' names actually filleted), checks {valid, solids, envelope_ok, envelope_growth_mm, envelope_tol_mm}, mode ('batch' | 'per_edge'), partial}; when partial is True, also skipped_edges and a warnings entry. On a failed check the feature is removed, the Body's Tip is restored, and BlendCheckFailed is raised naming the offending edges and the subset that does fillet cleanly.
Inputs: edges (required), feature (required), name, radius, +3 more.
pingPingARead-only
Check that the FreeCAD worker is alive. Returns 'pong' on success.
Inputs: workspace. Returns: result
plastic_collapsePlastic CollapseARead-only
Exact plastic-hinge collapse of a solid rectangular beam (NO solver) — the
closed-form twin the perfectly-plastic CalculiX solve
(fem_set_nonlinear_material) is gated against. The beam bends about the
width_mm axis (depth = height_mm). σ_y from yield_mpa or a Materials-DB
material. Elastic modulus S = b·h²/6, plastic modulus Z = b·h²/4, shape
factor Z/S = 1.5; yield moment M_y = σ_y·S, fully-plastic moment M_p = σ_y·Z.
support maps the collapse moment to a point load: 'cantilever' (M = P·L) or
'simply_supported' (central, M = P·L/4). With load_n the applied moment and
its margins to M_y / M_p (and the regime: elastic / partially_plastic /
collapsed) are returned. A perfectly-plastic FEM solve caps the surface stress
at σ_y and loses equilibrium at M_p; linear theory climbs past both — that
contrast is the gate. Escalate to fem_set_nonlinear_material for
non-rectangular sections or partial-plasticity fields.
Returns {support, S_elastic_mm3, Z_plastic_mm3, shape_factor, yield_mpa, yield_moment_nmm, plastic_moment_nmm, yield_load_n, collapse_load_n, applied_moment_nmm, margin_to_yield, margin_to_collapse, regime, fidelity, band_pct, valid_range_ok, warnings, escalate_to}.
Inputs: width_mm (required), height_mm (required), length_mm (required), load_n, +4 more.
plate_checkPlate CheckARead-only
Handbook bending of a uniformly loaded flat plate (NO solver) — the
"do I need FEM at all?" screen. shape: 'rectangular' (a_mm × b_mm, short side
drives; Roark/Timoshenko ν=0.3 coefficients σ=β·q·b²/t², δ=α·q·b⁴/(E·t³),
interpolated in a/b) | 'circular' (diameter_mm; exact closed forms).
support: 'simply_supported' | 'clamped' (all edges). E from youngs_gpa or a
Materials-DB material (which also supplies yield for yield_safety_factor).
Exact within thin-plate theory, and the limits are returned as flags
(thin_plate_ok: span/t ≥ 10; small_deflection_ok: δ ≤ t/2) — a tripped flag
means escalate to the CCX fem_* pipeline (escalate_to='fem_run').
Returns {shape, support, aspect_ratio, beta, alpha, sigma_max_mpa, deflection_max_mm, yield_safety_factor, thin_plate_ok, small_deflection_ok, fidelity, band_pct, valid_range_ok, warnings, escalate_to}.
Inputs: shape (required), pressure_kpa (required), thickness_mm (required), a_mm, +7 more.
pocketPocketA
Subtract a pad of length mm from the body. through_all ignores length.
through ('wall'|'body'): preferred over through_all. 'wall' ray-casts to the
first exit and cuts exactly one wall — right for solids AND shelled bodies;
'body' (legacy ThroughAll) punches every wall of a shell. Implies
direction='into_body'; result carries wall_depth_mm.
direction ('into_body'|'away_from_body', preferred over the legacy raw
reversed, honoured only when neither is set): the tool probes both
Reversed values and picks the one that does / does not remove material.
strict: raise instead of warn on a degenerate pocket.
Returns {handle, name, volume, removed_volume, volume_ratio}, plus warnings
ONLY when the pocket removed the whole body or nothing (strict=True makes
those errors). direction='away_from_body' never warns.
Inputs: sketch (required), name, length, strict, +5 more.
polar_patternPolar PatternA
Repeat a PartDesign feature around an axis.
axis: 'X'|'Y'|'Z' for body origin axes, or {handle, edge: tag|'EdgeN'}. angle_deg: total swept angle (default 360 = full circle). occurrences: number of copies (>=2). Includes the original.
Inputs: feature (required), axis, name, reversed, +3 more.
press_fit_stressPress Fit StressARead-only
Rate an interference (press/shrink) fit via Lamé. p=delta_r E (ro^2-rc^2)/ (2 rc ro^2); hub bore hoop=p(ro^2+rc^2)/(ro^2-rc^2); torque=2pi mu p rc^2 L. interference_mm is diametral.
E comes from material's Materials-DB card; youngs_modulus_mpa overrides it.
Every output is LINEAR in E, so a material the corpus has no modulus for is an
ERROR, not an assumption (#269) — the message names both exits.
pass is THREE-state: the hub yield check needs the card's yield_mpa, and a
card without one (Fused-Silica, Gold, Concrete, ...) leaves the check
unperformed. That returns pass=null with the reason in warnings — an
unperformed check is not a passed one. Test pass is True, not truthiness.
Returns {contact_pressure_mpa, hub_hoop_stress_mpa, torque_capacity_nm, axial_force_n, hub_yield_sf, youngs_modulus_mpa, youngs_basis, hub_yield_basis, pass, warnings}.
Inputs: shaft_dia_mm (required), interference_mm (required), hub_outer_dia_mm (required), engagement_length_mm (required), +4 more.
project_check_referencesProject Check ReferencesARead-only
Reference-integrity guard (issue #143 / D1) — catch a broken cross-file reference before a merge (the chronic PDM failure mode). Loads a project's assembly manifest + item registry and checks every reference resolves: a moved / renamed / missing component file, a dangling item-ref (an unknown id, or a resolved file missing on disk), an instance naming an unknown component, and naming-convention violations.
project: path to the project.json container.
Returns {ok, problems} — ok is True iff every reference is live, so a broken reference is reported here instead of as a cryptic merge failure.
Inputs: project (required), workspace.
project_resolve_manifestProject Resolve ManifestADestructive
Resolve a project's item-ref components to file components and write a merge-
ready manifest (issue #143 / D1, the deferred #140 seam). Each component naming an
item (items.json identity, not a bare path) is lowered to a {file} component
with the CAD artifact resolved from the registry, so merge_assembly consumes the
result unchanged — renaming/moving a file updates the item's files[] in one place
without breaking any reference.
project: path to the project.json container. out: output manifest path (defaults to .resolved.json next to it).
Returns {path, lowered} — the written path and the lowered manifest object; a dangling item-ref fails loudly.
Inputs: project (required), out, workspace.
project_validateProject ValidateARead-only
Validate a project.json container (issue #143 / D1): the schema stamp ("ankusdrive.project/1"), the naming convention on the project name, the conventional path fields, and — relative to the project directory — that the referenced manifest + item registry exist, the components directory is present, and a named master/skeleton is a real component of the assembly manifest.
project: path to the project.json container.
Returns {ok, problems, schema, name} — ok is True iff problems is empty.
Inputs: project (required), workspace.
publish_interfacePublish InterfaceADestructive
Record a named interface frame on a component so other parts can mate to it — the published "here is where you bolt to me, and how it's oriented".
handle: the component's shaped object. name: interface name (e.g. "lid_seat", "bolt_circle", "bore_axis"). frame: {origin:[x,y,z], z_axis:[...]?, x_axis:[...]?}. z_axis defaults +Z, x_axis +X. Extra keys (e.g. bolt-circle metadata) are stored verbatim.
Persists in the component's .FCStd as a JSON property bag, so merge_assembly can mate against it later. Returns {handle, name, frame, interfaces}.
Inputs: name (required), frame (required), handle (required), workspace.
query_facesQuery FacesARead-only
Filter faces by a structured predicate. Returns matching descriptors.
Predicate fields (all optional, ANDed):
kind / type: 'planar' | 'cylindrical' | 'conical' | 'spherical' | 'toroidal' | 'spline'
normal_dir: [x, y, z] unit vector for planar faces (with normal_tol)
radius_eq: float, matches cylindrical/conical/spherical (with radius_tol)
area_min / area_max: bounds in mm^2 Optional ordering:
centroid_max / centroid_min: 'x' | 'y' | 'z' (sorts result)
order: 'area_desc' | 'area_asc'
Example: {"type": "planar", "normal_dir": [0, 0, 1], "centroid_max": "z"} finds the topmost +Z-facing face.
Inputs: handle (required), predicate (required), workspace.
random_vibrationRandom VibrationARead-only
Random-vibration response off a modal run (Miles' equation; closed-form, no
external solver). Provide either analysis (a handle whose fem_modal + fem_run
already produced natural frequencies) or an explicit frequencies_hz list, plus
a base-acceleration PSD psd_profile ([{"hz":20,"g2_hz":0.01}, ...]; log-log
interpolated, and zero outside its band so a mode stiffened above the band
escapes drive). Each mode is an SDOF resonator with amplification q (default
10; rule of thumb Q≈√f_n), combined by SRSS: rms_g = sqrt(Σ (π/2)·f·W(f)·Q).
With modal_stress_mpa_per_g the g response converts to RMS / 3-σ stress; add
allowable_stress_mpa for a pass/fail.
Returns {rms_g, first_mode_hz, dominant_mode_hz, q, psd_band_hz, miles_grms_g, modes: [{mode, frequency_hz, psd_g2_hz, contribution_g, in_band}], rms_stress_mpa, three_sigma_stress_mpa, pass}.
Inputs: psd_profile (required), q, analysis, workspace, +3 more.
recipeBuild RecipeA
Build a registered part recipe into the active document (issue #136): "regenerate with new parameters" = "re-run the recipe." Validates {recipe, inputs} against the declared schema (typed units + ranges) at the door, then runs the deterministic build — geometry + publish_interface + declare_intent. Returns {recipe, schema, inputs, handle, name, interfaces, intent, part}.
Inputs: recipe (required), inputs, workspace.
recipe_listRecipe ListARead-only
List every registered part recipe — named, parameterized, declared-input build templates (issue #136). Returns {schema, count, recipes} where each recipe maps to {doc, required, optional, emits}; the cheap directory to browse before picking and parameterizing a recipe with recipe_schema / recipe.
Inputs: workspace.
recipe_schemaRecipe SchemaARead-only
Return one recipe's declared INPUT SCHEMA — its driving parameters with
type/unit/default/range. Returns {schema, recipe, doc, inputs:[{name, type,
unit?, default?, min?, max?, required, choices?, doc?}], emits}. recipe names
a registered recipe; an unknown name fails loudly. This is the contract a
parametric regeneration is authored against.
Inputs: recipe (required), workspace.
recipe_validateRecipe ValidateARead-only
Validate a recipe reference {recipe, inputs} WITHOUT building it — the cheap front door (mirrors validate_manifest). Catches an unknown recipe, a missing required input, and a wrong-typed / out-of-range / bad-unit / unknown input. Returns {ok, problems} — ok is True iff problems is empty. Run before building or merging to reject a malformed parameterization before geometry is spent.
Inputs: recipe (required), inputs, workspace.
register_handleRegister HandleA
Register an existing FreeCAD object into the AnkusDrive handle table. Use after run_script (when auto_register=False) or after open_document to bring objects into the handle ecosystem so subsequent tool calls accept them via handle.
object: the FreeCAD object's .Name (e.g. 'Helix001', 'Cut'). prefix: handle prefix (default 'manual'). Each call returns a fresh handle; registering the same object twice produces two aliases. Returns {handle, name, type, label}.
Inputs: object (required), prefix, workspace.
release_packageRelease PackageADestructive
Produce the vendor/RFQ deliverable bundle for one item at one revision — STEP + drawings + BOM + inspection package + a checksummed manifest — gated by the item's lifecycle state and stamped with its ECO.
Every piece of this exists as its own tool. What this adds is the guarantee that ties them together: the STEP, the PDF, the BOM and the title block all describe the SAME revision of the SAME item. That is the whole point of a release, and it is enforced BEFORE a single file is written:
the item must be in a releasable lifecycle state (
released), ordraftmust be set — which watermarks EVERY artifact PRELIMINARY (burned into the drawing, and carried as a format-legal comment in the STEP header, the DXF and the CSVs). Anobsoleteitem is refused in both modes.drawing_gatemust pass for every included page — with require_ballooned when the inspection kind is requested.the title block's part number / revision / material must MATCH the items registry. A mismatch is a FAILURE carrying expected-vs-actual, never a silent fix: quietly rewriting the print would destroy the only independent check that it and the model describe the same thing.
A refused release writes NOTHING — no half-populated directory a build script could mistake for a package.
registry / item: the items.json sidecar and the item id being released.
out_dir: where the bundle is written (created if absent).
kinds: any of "step", "drawing_pdf", "drawing_svg", "drawing_dxf", "bom_csv",
"inspection", "manifest_json". Default step/drawing_pdf/drawing_dxf/
bom_csv/manifest_json. manifest_json is always added; inspection
(ballooned print + plan + blank AS9102 form) implies a drawing kind.
draft: cut a PRELIMINARY package from an unreleased item.
rfq: the quote flavour — adds quantity breaks and the cost_estimate rollup as a
quote-COMPARISON baseline (fidelity "correlation", band_pct 100 — trust the
ratios, not the dollars) and drops internal-only artifacts (the inspection
package, which states your acceptance criteria).
eco: the change order this release is cut under; defaults to the item's
metadata.eco. Stamped into the manifest AND the title block's REV cell.
pages: page handles/names/labels (default: every page in the document).
handle / object: the geometry to export (default: the first page's main view
source — literally the solid the drawing dimensions).
process: drawing_gate process ("auto" | "prismatic" | "turned").
density / recursive: BOM options, as in bom_extract.
quantity_breaks / cost_process / material: RFQ pricing inputs (default
[1, 10, 100], "cnc", and the item's declared material).
Determinism: the same item at the same revision produces a BYTE-IDENTICAL package. The exporters' wall-clock header stamps (the STEP FILE_NAME timestamp above all) are scrubbed to a fixed epoch, so a re-released package is diffable by checksum.
Returns {ok, dir, item, part_number, rev, lifecycle, eco, material, draft, watermark, flavor, kinds, dropped_kinds, implied_kinds, pages, files:[{name, kind, bytes, blake2b}], manifest, manifest_path, verify, problems}. ok=False means the gate refused and nothing was written; each problem carries {code, where, field, expected, actual, reason}.
Inputs: item (required), out_dir (required), registry (required), eco, +13 more.
render_capabilitiesRender CapabilitiesARead-only
Report which photoreal renderers are usable right now — Blender (the studio backend) and the FreeCAD Render add-on renderers — so you can pick one for render_photoreal, or tell the user exactly what to install, instead of discovering availability by trial and error.
Takes no arguments and renders nothing. Resolves Blender like a solver
(ANKUSDRIVE_BLENDER_PATH -> PATH -> per-OS install dirs, incl. the versioned
Blender Foundation\Blender X.Y and /opt|~/.local/opt/blender-*) and launches it
once (cached) to read its version and GPU; resolves each add-on renderer as
render_photoreal would, changing no settings.
Returns {default_renderer ('auto'), auto_selects (the renderer 'auto' would use
now, or None), recommended ('Blender'), platform, available (ready renderer names
for renderer=), renderers: {name: {available, backend ('blender' |
'render_addon'), path or install_hint, ...}} — Blender adds version, device, gpu,
oidn, supported, scenes, qualities, devices, finishes — materials (appearance card
names), appearance_fields (the PBR dict keys), addon_importable, addon_error (when
the add-on does not import), and suggestion {renderer, why, install} while
Blender is missing}. When the user wants better renders, relay the Blender
install_hint verbatim — never run installers yourself. setup_status reports the
same install under the studio_render family.
Inputs: workspace.
render_fem_resultsRender FEM ResultsARead-only
Render a completed FEM result's surface, colored by a per-vertex field.
Pulls the result surface (boundary triangulation + per-node field values + displacement vectors) from the worker, then colors it with a viridis colormap (barycentrically interpolated), overlays the deformed shape, and draws a colorbar with the field min/max — the "agent eyes" for a stress / displacement / thermal solve.
field: 'vonmises' (default) | 'displacement' | 'temperature'. view: a preset ('iso'|'top'|'front'|…) or a custom '(azimuth,elevation)' camera passed as e.g. "45,35". deformation_scale: 'auto' scales the peak displacement to ~8% of the model diagonal; a number is used verbatim; '0' disables the deformed overlay.
Returns {png_base64, width, height, field, units, min, max, view, node_count, triangle_count}.
Inputs: analysis (required), view, edges, field, +4 more.
render_jobRender JobADestructive
Poll an async render started by render_photoreal_submit.
Returns {job_id, status} where status is 'running', 'done', or 'failed'. When 'done', also returns {png_base64, png_path, renderer, view, material, width, height}; when 'failed', {error}. The result remains available for repeat polls.
Pass discard=True once you have a terminal result to free the job immediately (drops the cached image and closes its temp document); ignored while running. Jobs are also auto-evicted oldest-first once finished jobs exceed an internal cap.
Inputs: job_id (required), discard, workspace.
render_photorealRender PhotorealA
Photorealistic "hero image" of a part or a whole assembly — presentation quality, unlike render_view's fast software-rasterized preview.
Call render_capabilities first when unsure what is installed. renderer='auto' (default) uses Blender (Cycles, full install, run headless) when it resolves, otherwise POV-Ray (or another FreeCAD Render add-on renderer). Name one to force it: 'Blender' | 'Povray' | 'Luxcore' | 'Appleseed' | 'Cycles' (the stripped standalone build, not Blender) | 'Ospray' | 'Pbrt'. Renders never modify the live model.
What to render: handle (a part, or an assembly handle — every leaf part is
rendered in place) or parts, a list of {handle, appearance?, name?} entries.
Appearance per part: appearances maps an assembly's part names (as
list_assembly_parts reports them) to an appearance; parts[].appearance sets it
inline; material is the fallback for every unassigned part. An appearance is a
card name ('Aluminium', 'Brass', 'Carpaint', 'Emission', 'Glass', 'GlossyPlastic',
'Gold', 'Iron', 'Matte', 'RoughPlastic', ...) or a neutral PBR dict {base?, color
('#rrggbb' or linear [r,g,b]), metallic, roughness, emission, emission_strength,
transmission, ior, coat, finish ('none'|'fdm_layers'|'brushed'),
layer_height_mm}. Unknown cards/fields raise listing the valid ones.
Blender-only (reported under ignored on an add-on renderer): per-part
appearance, scene ('studio': seamless cyclorama, soft key/fill/rim area lights,
contact shadows), quality ('draft' 16 | 'preview' 64 (default) | 'final' 384
samples, OIDN-denoised), device ('auto' GPU-if-present | 'cpu' | 'gpu'), and
output_dir (where render.png + render.blend are written; default a temp dir).
view: 'iso' | 'top' | 'bottom' | 'front' | 'back' | 'left' | 'right' | 'side'.
Returns {png_base64, png_path, renderer, auto_selected, view, material, width,
height}. The Blender backend adds {blend_path, scene, quality, samples, denoised,
device, blender_version, elapsed_s, parts: [{name, material, triangles}]}: open
blend_path in Blender 5.1+ (optionally with Blender's own MCP server connected) to
art-direct the scene further — AnkusDrive does not drive that server. When 'auto'
falls back off Blender the result carries suggestion {renderer, why, install}:
relay its install command to the user verbatim rather than installing anything.
Not installed: Blender/'auto' return {ok: False, renderer, status, reason,
install}; a named add-on renderer raises with install guidance.
Presentation-only: output is not bit-reproducible, so it is kept out of the reliability/golden tests. Renders can take seconds to minutes (extended worker timeout); prefer render_photoreal_submit for quality='final'.
Inputs: view, parts, scene, width, +9 more.
render_photoreal_submitRender Photoreal SubmitA
Start a photorealistic render asynchronously; returns immediately with {job_id, status, renderer} instead of blocking for the whole render.
Use this (rather than render_photoreal) for renders that may take a long time — quality='final', large images, big assemblies — so the worker stays responsive. Poll render_job(job_id) until status is 'done' (then it returns the PNG and, for Blender, blend_path) or 'failed'. Same arguments, renderer selection and not-installed miss dict as render_photoreal; see render_capabilities for what resolves on this machine.
Inputs: view, parts, scene, width, +9 more.
render_viewRender ViewBRead-only
Render an isometric/orthographic view of a shaped object as a PNG.
view: 'iso' | 'top' | 'bottom' | 'front' | 'back' | 'left' | 'right' | 'side'. deflection: tessellation accuracy in mm (smaller = finer mesh, slower). edges: draw triangle edges over filled faces.
Returns {png_base64, width, height, view, vertices, triangles}.
Inputs: handle (required), view, edges, width, +3 more.
render_viewsRender ViewsCRead-only
Render multiple views of a single object. Returns {views: {view_name: {png_base64,...}}}. Default views: ['iso', 'top', 'front'].
Inputs: handle (required), views, width, height, +2 more.
resolve_edgeResolve EdgeBRead-only
Resolve an edge tag to the current EdgeN index. Errors on miss or ambiguity.
Inputs: tag (required), handle (required), workspace.
resolve_faceResolve FaceARead-only
Resolve a face tag to the current FaceN index. Errors on miss or ambiguity.
Use this when you need to pass a (object, 'FaceN') reference into a tool that doesn't accept tags directly (e.g. legacy FEM constraints).
Inputs: tag (required), handle (required), workspace.
restart_workerRestart WorkerADestructive
Kill the current workspace's FreeCAD worker process and spawn a fresh one. Use when the worker is wedged (e.g. App.ActiveDocument desynced from internal state). All open documents, unsaved changes, and handles in THIS workspace are lost — save first if needed. Other workspaces are untouched. Returns {restarted: True, workspace: , freecad: [...]}.
Inputs: workspace.
revolveRevolveB
Revolve a sketch around a body origin axis ('X'|'Y'|'Z') by angle deg.
Inputs: sketch (required), axis, name, angle, +2 more.
rigid_sphere_scatteringRigid Sphere ScatteringARead-only
Exact rigid-sphere plane-wave scattering far-field form function via the Mie
series (NO solver) — the closed-form twin the Bempp exterior-acoustics BEM
scattering solve (acoustic_radiation_submit, problem='scattering') is gated
against. For compactness ka and scattering angle theta_deg (from the forward
direction; 180° is backscatter), f∞(θ) = (2/ika)·Σₙ(2n+1)[−j'ₙ(ka)/h'ₙ(ka)]·
Pₙ(cosθ) — a rigorous spherical-harmonic sum (Neumann ∂p/∂r=0 on the sphere)
truncated past convergence (fidelity='exact'). The backscatter |f∞(π)| → 1 in
the geometric (ka≫1) limit and rises through the resonance region. A BEM
scattered far field must land on |f∞(θ)|.
Returns {ka, theta_deg, a_m, form_function_abs, form_function_re, form_function_im, backscatter_abs, n_terms, fidelity, band_pct, valid_range_ok, warnings, escalate_to}.
Inputs: ka (required), a_m, theta_deg, workspace.
run_scriptRun Python ScriptADestructive
Escape hatch: execute Python in the worker with App/Part/ObjectsFem in scope.
Set __result__ in the script to return a JSON-serializable value.
auto_register (default True): any new shape-bearing object the script
creates is automatically registered into the handle table. The result
includes a registered list of {handle, name, type} entries so the next
tool call (render_view, list_faces, fillet_edges, mass_properties, etc.)
can address script-created objects via handle without a separate
register_handle round-trip.
Disabled unless ANKUSDRIVE_ALLOW_RUN_SCRIPT permits it (the Claude Desktop extension defaults it off); when disabled this tool is not registered at all.
Returns {result, registered}.
Inputs: code (required), workspace, auto_register.
save_documentSave DocumentADestructive
Save the active document to the given .FCStd path.
visibility_hygiene (default True): before saving, hide any object that has been consumed as a producer-input (the Base/Tool of a Cut, the BaseFeature of a Body, every feature inside a Body's Group, etc.). Without this the re-opened doc double-renders intermediates on top of the final shape — a failure mode that looks identical to broken geometry. Pass False to keep explicit set_visibility overrides intact. attach_provenance: embed this document's journal_export record (PRIVACY.md).
Inputs: path (required), workspace, attach_provenance, visibility_hygiene.
scaffold_projectScaffold ProjectADestructive
Lay out a well-formed project in one call (issue #143 / D1) — promote the MULTI_AGENT.md §3/§7 directory convention to a primitive. Creates the convention directories (components/, .dp_lib/), an item registry (items.json, #140), a seed assembly manifest from the supplied components/instances, and the project.json container that ties them together. The result loads clean and merge_assembly consumes it unchanged once its components resolve.
base_dir: the project root directory (created if absent). name: the project id (naming-convention checked). components/instances/shared_parameters: the assembly manifest content. master: optional component id to record as the master/skeleton single-source-of- truth slot (the lean interface-geometry skeleton children mate against). items: optional {item_id: {files?, metadata?}} to seed the item registry with.
Returns the layout {project_file, manifest, registry, components_dir, lib_dir, lockfile, master, dirs}.
Inputs: name (required), base_dir (required), items, master, +4 more.
scale_shapeScale ShapeA
Scale a shape uniformly or per-axis, baking a fresh static solid.
Scaling breaks parametric history, so this produces a standalone Part::Feature (not a linked/parametric feature); the source object is hidden since its geometry is consumed into the scaled copy.
handle: source shape handle. factor: scalar for uniform scale, or [sx, sy, sz] for per-axis scale. All factors must be > 0. center: optional [x, y, z] mm pivot to scale about; when omitted the scale is about the world origin (so the shape also moves away from/toward origin). name: object label (default 'Scaled').
Lengths in mm. Returns {handle, name, volume, factor} where factor is the
normalized [sx, sy, sz] applied and volume (mm^3) equals the source volume
times sxsysz.
Inputs: factor (required), handle (required), name, center, +1 more.
seal_checkSeal CheckARead-only
Rate an O-ring gland (pairs with oring_groove): squeeze=W-depth, fill= (pi/4 W^2)/(width*depth). Squeeze must sit in the application band (static 15-30%, dynamic 10-20%), fill below max_gland_fill_pct. Returns {squeeze_mm, squeeze_pct, gland_fill_pct, squeeze_range_pct, within_squeeze, within_fill, pass}.
Inputs: groove_depth_mm (required), groove_width_mm (required), cross_section_dia_mm (required), workspace, +2 more.
section_viewSection ViewA
Cut a solid with a plane and measure the exposed cross-section — the way to "see inside" a part blind. Does not modify the input. mm / mm^2.
plane: "XY", "XZ", "YZ" (normals +Z, -Y, +X, as FreeCAD) or a datum-plane handle (its local +Z). offset: plane shift along its normal, mm (plane="XY", offset=10 cuts z=10). emit_profile: also add the section wires as a new registered object and return its handle (raises if the plane misses). Default: measure only. name: the emitted profile's name.
Returns {plane, offset_mm, normal, section_area_mm2 (closed wires), wire_count (0 = plane misses), closed_wire_count, bbox ({min, max, size} or None), handle?, name? (only with emit_profile)}.
Inputs: handle (required), name, plane, offset, +2 more.
session_transcriptSession TranscriptARead-only
Export this session as a Python script that regenerates it (model, drawings, simulations) by calling these same tools in a fresh worker — and, with it, the record of what computed each number. Read-only: returns the script as text and writes nothing.
Covers one workspace (default: the current one). In the script, handles are variables, job polls are one s.wait(job), and inspection calls are dropped unless a later call used their result. Absolute paths become WORKDIR-relative. The header lists failed calls, run_script use and input files.
THE AUDIT SURFACE. Analysis calls are never dropped — every hand-calc, FEM read and *_submit is in the script, because they are the derivation, not inspection. Each analysis and solve result is checked whole: every number it reported becomes an s.check() and its verdict fields (fidelity, correlation, solver, gate pass) become s.expect(), so a replay that reached a different solver or fell back to a different correlation stops there instead of returning a plausible number. run_script code is carried verbatim with its SHA-256. With provenance (default), the script opens with a PROVENANCE record — AnkusDrive/FreeCAD/platform/substrate plus every solver the session reached, with its resolved path, substrate and probed version — and calls s.provenance(), which re-resolves that on the replaying machine and prints every difference. Hand this to anyone who has to answer "what exactly produced this figure?".
include_read_only: keep every inspection call. checkpoints: emit s.check()/s.expect(). prune_aborted: drop calls a transaction_abort undid. provenance=False skips the environment record (and the solver version probes it runs, ~0.1-3 s per solver).
Returns {workspace, script, calls, exported, skipped: {reason: count}, warnings,
prerequisites, truncated, provenance}. Run it as python transcript.py [WORKDIR].
Inputs: workspace, provenance, checkpoints, prune_aborted, +1 more.
set_active_documentSet Active DocumentA
Switch the active document by name (the value returned from new_document/open_document).
Inputs: name (required), workspace.
set_propertySet PropertyCDestructive
Set a single property by name on an object. Coerces lists → Vector for Vector properties; other values pass through.
Inputs: name (required), value (required), handle (required), workspace.
set_title_blockSet Title BlockADestructive
Populate the drawing's title block (composed bottom-right on SVG/PDF export; calling this opts the page into rendering it). Scale, sheet size, units and part name are auto-derived; these fields override or add to them. Returns {handle, name, fields}.
Inputs: page (required), rev, date, part, +5 more.
setup_statusSetup StatusARead-only
The machine-readable form of ankusdrive doctor — resolve FreeCAD and every
solver family and report, per item, found/missing with the exact fix. Call this
when the user asks to set up, diagnose, or finish installing AnkusDrive, then walk
them through the per-item fix/install_hint commands for their platform.
Read-only and side-effect-free: nothing is executed or installed and no
environment is mutated (verify_freecad_boot=True additionally boots FreeCAD
once, time-boxed, purely to read back its version — leave it False unless the
user doubts the install actually runs).
verify_image=True additionally checks, OVER THE NETWORK, that the solver
container's image was signed by this repository — use it when the user asks
whether the solvers they are running are authentic. It adds
container_image.verification = {status, reason, repo, workflow,
allowed_by_config, self_declared}, where status is:
• verified — provenance names this repository's image workflow;
• unsigned — no attestation: an image the user built themselves (common and
legitimate), one published before signing existed, or no network/gh. Relay
it as expected-for-a-custom-image, and mention
ANKUSDRIVE_ALLOW_UNVERIFIED_IMAGE=1 silences it;
• mismatch — an attestation exists but names a DIFFERENT repository. Say so
plainly: that is an image claiming to be ours. No setting silences it;
• unavailable — the check could not run (reason says why). Never report
this as authentic.
self_declared is what the image says about itself and is never evidence —
only the signature is.
Returns {platform: {system, machine}, freecad: {available, path, source,
version?, fix?}, install: {kind, source} (venv | pipx | uv_tool | uvx | mcpb —
the install every fix string is written for), toolsets: {enabled, disabled:
{family: {label, tools, enable}}} (tool families switched off in this install and
exactly how to switch each on), run_script: {allowed, enable?} (whether the
run_script tool may execute agent-written code here), solvers: {available, unwired, prepared_case_only, solvers:
{name: {..., install_hint | wire_hint}}, families: {family: {solvers, available,
unwired, prepared_case_only, any_available}}, extras}} —
families[*].any_available is what gates each *_submit family, and every
unavailable item carries its own fix string. A solver under
prepared_case_only resolves but no AnkusDrive tool can build it a case, so it
does not make its family available (SU2/cfd — issue #237).
Inputs: workspace, verify_image, verify_freecad_boot.
set_visibilitySet VisibilityADestructive
Override an object's persistent Visibility flag. By default save_document auto-hides producer-inputs (the Base/Tool of a Cut, features inside a Body) so the re-opened doc shows just the final composition. Use this to override — e.g. to keep a reference primitive visible next to a derived part. Note that the next save_document with visibility_hygiene=True (the default) may re-hide it; pass visibility_hygiene=False to save_document to lock the override in.
Inputs: handle (required), visible (required), workspace.
sheet_baseSheet BaseA
Start a sheet-metal part: the base flange, a closed straight-sided profile
extruded to thickness_mm. Everything else (flanges, tabs, hems, the flat
pattern, the DXF) hangs off the handle this returns.
profile: [[x, y], ...] in the XY plane, implicitly closed. sketch: alternatively a handle to a closed, planar, straight-sided sketch — on any plane. Arcs are REJECTED rather than silently faceted, because a faceted flat pattern is a wrong flat pattern. material: any Materials-DB name (e.g. "Steel-A36", "AL6061-T6", "SS304"); it selects the K-factor and minimum-bend-radius corpus rows.
IMPORTANT — the profile is the flat face TANGENT TO TANGENT, not the outside dimension. Bends grow OUTWARD from the profile boundary, exactly as a base flange behaves in any sheet-metal CAD, so a U-channel of 100 mm outside width with R = t = 2 starts from a 92 mm profile.
The bend model lives alongside the handle for the life of the worker session, like every other handle: unfolding is a property of the FEATURE TREE, not of the fused solid, so a part reopened from disk in a new session is a solid rather than a sheet part. Cutting one (boolean_op cut, e.g. to drill it) keeps it a sheet part; fusing arbitrary material onto it does not.
Returns {handle, name, volume, thickness_mm, material, profile, area_mm2}.
Inputs: thickness_mm (required), name, sketch, profile, +2 more.
sheet_checkSheet CheckARead-only
Press-brake manufacturability screen for a sheet part — four rules, each with the number it came from:
min_bend_radius — an inside radius below the material's minimum (a corpus value per material: 3t for 6061-T6, 1t for A36, 0.5t for annealed 1100) cracks the outer fibre. min_flange_length — an outer leg under 4t + R has no die shoulder to sit on and dives into the vee. hole_to_bend — a hole whose EDGE is nearer the bend tangent than 2t + R draws into an oval. Holes are read off the real solid, not declared. refold_collision — two features that occupy the same space once folded, found by actually intersecting them rather than by a rule.
A hem is screened as a two-hit hem (bend, then flatten) and exempted from the air-bend radius and flange rules, which would otherwise fail every hem ever drawn. A flat pattern whose feature footprints overlap is a finding too, not a warning dropped on the floor. An unrecognised material degrades to a bend-class fallback WITH an info finding saying so, rather than skipping the rule silently.
min_flange_t / hole_to_bend_t: override the thresholds (multiples of thickness).
fidelity='correlation' with band_pct=None — these are press-brake rules of thumb, thresholds for ranking and gating rather than measured predictions.
Returns {ok, findings, fail_count, rules, min_bend_radius_mm, min_bend_radius_source, flat_size, blank_area_mm2, k_factors, thickness_mm, material, fidelity, band_pct}. Each finding carries {code, severity, message} plus the measured value and the limit it missed.
Inputs: handle (required), k_factor, workspace, bend_table, +2 more.
sheet_flangeSheet FlangeA
Bend a flange off a free edge of a sheet part.
edge: a stable e_* tag from list_edges (or 'EdgeN'/int) on a STRAIGHT free edge
of a flat region — a bend line is the intersection of two planes, so an arc
cannot carry one and is rejected.
angle_deg: the bend angle, i.e. the deviation from flat, so 90 is a right-angle
flange. Must be in (0, 180].
inner_radius_mm: inside bend radius; defaults to the material thickness.
direction: 'up' (toward the region's outward normal) or 'down'.
length_from: what length_mm measures — the number most often misread on a
sheet drawing. 'outer' (default) to the outside virtual apex, which is what
a drawing dimension normally means; 'inner' to the inside apex; 'tangent'
for the straight leg past the end of the bend.
width_mm / offset_mm: narrow the flange to part of the picked edge.
k_factor: pins K for THIS bend only. Leave it unset and the choice defers to
sheet_unfold — the folded solid does not depend on K at all, only the flat
pattern does.
Consumes the input handle (it is hidden, having become part of the result).
Returns {handle, name, feature, kind, volume, angle_deg, inner_radius_mm, leg_tangent_mm, length_from, direction, span_mm, thickness_mm}.
Inputs: edge (required), handle (required), length_mm (required), name, +9 more.
sheet_flat_exportSheet Flat ExportADestructive
Write the flat pattern as a LAYERED DXF — the file a laser, punch or press-brake shop actually quotes and cuts from. This is the deliverable the whole sheet-metal family exists to produce.
Three layers, because a flat pattern without them is not a shop deliverable:
CUT carries the closed outer profile and every hole; BEND_UP and BEND_DOWN carry
one centreline per bend, so the operator reads the fold direction off the print
rather than inferring it. DXF R12 ASCII in millimetres, written directly rather
than through TechDraw — a flat pattern is not a drawing view and does not want a
sheet frame, a scale or a title block around it. path must end in .dxf.
Takes the same k_factor / bend_table arguments as sheet_unfold, since the outline it writes IS the development.
Returns {ok, path, size, layers, entities, flat_size, blank_area_mm2, bends (with bend_allowance_mm, bend_deduction_mm, k_factor and k_source per bend), fidelity, band_pct, warnings}.
Inputs: path (required), handle (required), k_factor, workspace, +1 more.
sheet_hemSheet HemA
Fold a hem back on itself — the 180-degree return that stiffens a free edge and buries the sharp cut line so the part is safe to handle.
kind: 'closed' (inside radius t/2, gap t) or 'open' (radius t, gap 2t). radius_mm / gap_mm: override the style directly; the gap between the returned leg and the parent is exactly 2R, so gap wins as radius = gap/2.
length_mm is ALWAYS the return leg measured from the end of the bend: a 180-degree bend has no virtual apex to dimension to — the outside surfaces are parallel and never meet — so an 'outer' dimension would be infinite. For the same reason a hem reports a bend allowance but no bend deduction, and sheet_check screens it as a two-hit hem (bend, then flatten in a hemming die) exempt from the air-bend radius and flange rules. A teardrop hem wraps past 180 degrees and is out of scope.
Returns the same dict sheet_flange does, plus {hem_kind, gap_mm}.
Inputs: edge (required), handle (required), length_mm (required), kind, +8 more.
sheet_refoldSheet RefoldA
Fold a flat pattern back up and check it reproduces the part — the other half of the unfold gate.
This does NOT replay the feature model. It reads the flat pattern back: each leg length is measured off the flat outline, walking outward from the bend's attachment past its reported bend allowance to the far edge of that region. So a wrong allowance, angle or bend direction lands the refolded solid somewhere the original is not, and this reports the disagreement instead of hiding it.
handle: a sheet part to unfold and then refold.
flat: alternatively a sheet_unfold report, to refold a development produced
elsewhere (or a deliberately corrupted one, to prove the check bites).
compare: the handle to check against; defaults to handle, and is skipped when
only flat is given.
volume_tol_pct / bbox_tol_mm: agreement tolerances.
Note the round trip that is meaningful and the one that is not: refold-vs-folded must match, but flat-vs-folded VOLUME must not, and does not. Bending preserves neutral-fibre length, not material volume — a bend sector's true volume is angle·t·(R + t/2)·w while its flat footprint is angle·(R + K·t)·t·w, and those agree only at K = 0.5.
Returns {handle, name, volume_mm3, bbox, bends, compare?} where compare is {handle, matches, volume_mm3, volume_error_pct, bbox_max_error_mm, tolerance}.
Inputs: flat, name, handle, compare, +5 more.
sheet_tabSheet TabA
Extend a sheet part with a coplanar tab — a flat ear off a free edge with no bend at all (a mounting lug, a weld tab, a snap-off).
Mechanically it is a zero-angle flange and shares that code path exactly:
length_mm is how far the tab reaches past the edge, width_mm/offset_mm
place it along the edge (default: the whole edge). It adds no bend to the bend
report and no bend line to the DXF, but it does grow the flat pattern.
Returns the same dict sheet_flange does (angle_deg 0, inner_radius_mm 0).
Inputs: edge (required), handle (required), length_mm (required), name, +4 more.
sheet_unfoldSheet UnfoldA
Develop a sheet part into its flat pattern — the blank the part is cut from — and report every bend.
The flat pattern is derived from the bend tree, not reverse-engineered out of the fused solid, so it is exact rather than fitted: each bend contributes its bend allowance BA = angle·(R + K·t), the arc length of the neutral fibre.
k_factor: pins K for every bend. bend_table: a shop's own measured rows [{thickness_mm, inner_radius_mm, angle_deg, allowance_mm | deduction_mm}] — a matching row OUTRANKS the chart, because the shop's press is the ground truth for the shop's press. Without either, K comes from a press-brake corpus keyed by material and r/t. WHICHEVER IT IS, IT IS ECHOED BACK per bend as k_factor + k_source: a flat length whose K you cannot see is a number you cannot check.
build: also create the flat blank as a real solid (holes included) at origin
in the XY plane, so it can be measured, exported or nested.
Fidelity: 'exact' only when EVERY bend's K was supplied or table-derived — BA
given K is pure arithmetic. One corpus-defaulted bend makes the development a
'correlation' with band_pct, and developed_band_mm gives the resulting
millimetre spread of the blank across that K band.
Returns {ok, handle?, name?, volume?, outline, holes, bend_lines, bends,
regions, flat_size, flat_bbox, flat_area_mm2, blank_area_mm2,
blank_volume_mm3, thickness, material, fidelity, band_pct, developed_band_mm,
warnings} — regions being each flat region's polygon, which is what lets the
whole report be handed straight back to sheet_refold. Each
bend row carries angle_deg, direction, inner_radius_mm, leg_tangent_mm,
outer_length_mm, bend_allowance_mm, bend_deduction_mm, outside_setback_mm,
k_factor, k_source, and the bend_line / tangent_start / tangent_end segments in
flat coordinates. ok=False means the blank cannot be cut as drawn — two feature
footprints overlap — with warnings naming which.
Inputs: handle (required), name, build, origin, +3 more.
shell_solidShell SolidA
Hollow a raw Part solid into a thin-walled shell (thickness does the same
for PartDesign bodies).
faces: NON-EMPTY list of faces to REMOVE (the openings) — f_* tags (preferred), 'FaceN', or 1-based ints. thickness: wall mm (> 0), grown INWARD so outer dimensions are kept.
The input is hidden. Returns {handle (starts 'shell_'), name, volume (mm^3), wall_thickness, removed_faces}. Raises on empty faces, thickness <= 0, a bad index, or an offset too large for a valid shell.
Inputs: faces (required), handle (required), thickness (required), name, +1 more.
slice_estimateSlice EstimateARead-only
First-order FDM slice estimate (analytic, NO slicer needed; see slice_gcode_submit for the real PrusaSlicer CLI). mass_g = volume·density (Materials DB); deposited = volume·(wall_fraction + infill·(1−wall_fraction)) so at 100% infill filament_g == mass_g; layer_count = ceil(bbox height/layer_height); print_time from nozzle volumetric flow.
material may be any Materials-DB card name (material_list / material_get), or
anything at all if you supply density_g_cc yourself — it overrides the DB
lookup. A generic word like 'polymer' is a CATEGORY, not a card, and 'nylon' is
a near-miss for one, so neither carries a density; the error names both exits.
filament_dia_mm (1.75 default, 2.85 for the older standard) sets the spool
stock the filament length is computed against.
Returns {mass_g, filament_g, deposited_volume_mm3, layer_count, print_time_min, infill_fraction}. Errors on a material with no density and no override, a negative volume, a bbox shorter than [x,y,z], or a non-positive layer height.
Inputs: bbox_mm (required), volume_mm3 (required), material, nozzle_mm, +7 more.
slice_gcode_submitSlice G-code SubmitA
Slice a real part with the PrusaSlicer CLI, asynchronous — the external-CLI
upgrade of the analytic slice_estimate: real perimeters, infill patterns,
supports, travel/acceleration, and the slicer's own print-time model.
Requires a PrusaSlicer install ('apt install prusa-slicer' / the AppImage);
when absent this returns {ok:false, reason, install} rather than raising.
Pass a body handle (exported to STL in the modeller) or a prepared
stl_path. infill_fraction is 0..1 (full infill auto-switches the fill
pattern — PrusaSlicer's default refuses 100%); material/density_g_cc set
the filament density used to turn the sliced volume into grams. With a body
the result also carries the analytic estimate and the deposited_ratio
between them (a 20 mm cube at 100% lands ~1.008 — the skirt).
Returns the degradation dict or {job_id, status, cache_hit}; poll job_result for {ok, gcode_path, filament_mm, filament_cm3, filament_g, print_time_s, print_time_text, layer_count, config (the slicer's echoed settings), analytic?, deposited_ratio?}.
Inputs: body, material, stl_path, supports, +5 more.
solve_capabilitiesSolve CapabilitiesARead-only
Report which P2 external solvers (CFD/MBD/topology/transient-thermal/optics) are usable right now — so you can pick a working solver for a *_submit family instead of discovering availability by trial and error. The solver twin of render_capabilities.
Takes no arguments. Resolves each solver side-effect-free: a binary by ANKUSDRIVE__PATH env -> PATH -> per-OS install dirs; a pip-wheel solver by importability. It executes nothing and installs nothing.
families[*].any_available is the gate to trust: it means AnkusDrive can actually
DRIVE that family here, not just that a binary resolved. A solver that resolves
but that no AnkusDrive tool can build a case for is listed under the family's
prepared_case_only (with the reason on the solver entry) and does NOT set
any_available — today that is SU2, which only ever runs a case_dir you
prepared yourself (*.cfg + *.su2); every built-in CFD case mode is OpenFOAM-only.
Returns {platform, available (sorted ready solver names), unwired,
prepared_case_only, solvers: {name: {available, kind ('binary'|'wheel'), family,
extra, and either path/module (when available) or install_hint, plus
prepared_case_only when nothing can build it a case}}, families: {family:
{solvers, available, unwired, prepared_case_only, any_available}}, extras:
{extra: [solver names]} for pip install ankusdrive[<extra>], toolsets: {enabled,
disabled: {family: {label, tools, enable}}}, cases: {root, count, bytes,
bytes_exact, keep, max_gb, grace_s, reaping}}. A family listed under
toolsets.disabled has no tools registered in this session — tell the user its
enable instruction rather than concluding the capability does not exist.
cases is where every generated solver deck is written (one managed root) and the
retention it is held to: at most keep directories and max_gb GB, reaped
oldest-first, never touching one younger than grace_s seconds. Point a user at
cases.root when they ask where a solve's files went (issue #437).
Inputs: workspace.
spring_checkSpring CheckARead-only
Rate a helical compression spring (Wahl). rate k=G d^4/(8 D^3 Na); corrected shear tau=Kw 8FD/(pi d^3). Pass force_n OR deflection_mm. Returns {spring_index, wahl_factor, rate_n_mm, force_n, deflection_mm, shear_stress_mpa, slenderness, buckling_flag, allowable_shear_mpa, shear_sf, pass}.
G and the allowable come from material unless overridden. shear_modulus_mpa
sets G directly; allowable_shear_mpa replaces the 0.45·UTS estimate, which
falls back to 700 MPa (spring steel) for a material carrying no UTS — pass it
explicitly for anything else, since shear_sf and pass scale with it.
Inputs: wire_dia_mm (required), active_coils (required), coil_mean_dia_mm (required), force_n, +6 more.
standard_part_designateStandard Part DesignateARead-only
The canonical, orderable designation of a purchased standard part — the string a buyer can actually quote against: "ISO 4762 M4×12 A2", "608-2RS", "AS568-214 NBR70". A BOM row that reads "SocketHeadCapScrew" is not a buyable line; this is what turns it into one.
Three ways in: handle read the designation add_fastener / add_bearing / add_thread stamped on the part when it was built. An object with no stamp reports designation=None plus whether its NAME reads like a purchased part. Nothing is ever inferred from geometry, so a hand-modelled bracket cannot acquire a false designation. designation normalise/parse a string — 'iso4762 m4x12 a2' becomes 'ISO 4762 M4×12 A2', so two spellings of one part can never become two BOM lines. family+spec build one from facts. family is fastener | bearing | oring | threaded_rod, and spec is respectively {kind, size, length, grade} / {designation, seals} / {inner_diameter, cross_section, compound} / {diameter, pitch, length, grade}.
Offline and deterministic — no network, no supplier, no credentials.
Returns the designation card: {ok, family, standard, designation, complete,
reason, purchased, ...family-specific fields}. complete=False means the string is
not yet enough to order against (typically nobody said which material grade),
with reason naming the gap — a missing fact is reported, never defaulted to a
plausible-looking lie.
Inputs: spec, family, handle, workspace, +1 more.
study_submitStudy SubmitA
Sweep parameters over a sampled design space and record the WHOLE search as a table — the DOE primitive between the parametric layer and the solver catalog.
Without it, exploring a design space means hand-rolling recipe(params) -> solve ->
mutate -> repeat and keeping only the last point, so nobody can tell afterwards
whether the design is good or merely the one you stopped on. A study keeps every
point, with the parameters that produced it.
variables declares the space; each entry is either explicit levels or a range:
{"name": "diameter_mm", "values": [8, 10, 12]}— these and only these{"name": "diameter_mm", "min": 8, "max": 12, "levels": 3}— evenly spaced
sampling picks how they combine:
{"method": "grid"}(default) — full factorial. Exhaustive, and the only thing that can prove a trend, but it is the PRODUCT of the level counts: three variables at five levels is 125 evaluations.{"method": "lhs", "n_samples": 20, "seed": 0}— Latin hypercube. Each variable's range is cut inton_samplesstrata and every stratum used once, so cost is decoupled from dimensionality: 20 points cover 6 variables as well as 2. Use it past 2-3 variables. Both are deterministic fromseed, which is what makes a re-run hit the cache.
responses says how to measure each point, in the same mapping a performance
requirement uses: {"name": "dp", "tool": "cfd_pipe_flow", "metric": "pressure_drop_pa", "conditions": {"diameter_mm": "$diameter_mm", "length_mm": 200}}.
Inside conditions, "$<variable>" is that point's value and "$handle" is the part
it built; an unknown $token is refused up front, because a sweep that silently
measured a literal string at every point returns a flat, plausible, wrong table.
tool can be ANY AnkusDrive tool, including verify_performance — and that is the
interesting case. A response that is a contract verdict carries a band, a trust block
and a pass/fail/indeterminate state, so points stay comparable across fidelity tiers
instead of being bare floats of unknown quality.
recipe (+ fixed_inputs) rebuilds geometry per point; omit it and pass handle
(or nothing) to sweep analysis parameters against fixed geometry. Screening-tier
responses evaluate inline in milliseconds, so thousand-point studies are viable;
solver-tier responses fan out concurrently and one collector job joins them.
Caching IS resumability: identical points hash to the same job content key, so
re-submitting a study after a crash, or widening its grid, re-runs only what is new —
n_cached is what tells you the re-run was free. max_points (default 64) refuses a
sweep larger than you probably meant.
objective — {"response": "dp", "sense": "min"} — additionally reports best.
Returns EITHER the finished table or {job_id, status, points, pending}; poll
job_result for the completed table. A point that failed to build or measure is a row
with ok: false, never an exception. Result: {ok, n_points, n_evaluated, n_cached,
n_failed, sampling, variables, points: [{index, params, handle?, ok, responses:
{name: {ok, value, band_pct?, converged?, state?, job_id?, cache_hit?, detail?}},
warnings}], responses: {name: {n, n_missing, min, max, mean, argmin, argmax}},
best?}.
Inputs: responses (required), variables (required), handle, recipe, +5 more.
substitutability_checkSubstitutability CheckARead-only
Liskov-substitutability gate — Form/Fit/Function as code (§7.1, #147). Take an assembly that gates green with variant A in a slot, swap in variant B (a different family row, or any part claiming the same interface), and re-run merge_assembly + all the gates. Still green ⇒ B is interchangeable with A — by construction a compatible (MINOR/PATCH) change ⇒ revise the existing part number; a gate now fails ⇒ the swap broke Form/Fit/Function ⇒ a new part number. Purely deterministic (no API, no judgment); the substitutability test #138 (B1 families) and #146 (the interface registry) call.
manifest: path to a manifest that gates green with variant A in slot.
slot: the component id to swap (variant A → variant B).
variant: the replacement component spec — a dict with exactly one of
file/manifest/library (the same one-source rule merge_assembly enforces).
verify_baseline: re-merge the base assembly first and require it green so the
premise is honest (default True).
Function, not just Form and Fit (#261): if the swapped-in variant declares a
PERFORMANCE contract (#226), it is part of the comparison. A contract measured as
NOT met breaks the performance gate like any other. A contract with NO recorded
verdict yields a THIRD answer — substitutable: null, verdict
'performance_unproven' — because an unverified spec is not a passed spec, and
handing an unproven part an existing part number is the silent pass #226 prevents.
Returns {schema, slot, variant, baseline_ok, swap_ok, substitutable (True | False | null), verdict ('substitutable' | 'not_substitutable' | 'baseline_not_green' | 'performance_unproven'), broken_gates (the NAMED gate(s) the swap broke), broken (gate→violations), classification (compatibility/semver/decision), performance? (only when a component declares a contract), reports}.
Inputs: slot (required), variant (required), manifest (required), workspace, +1 more.
suggest_looseningSuggest LooseningARead-only
The loosest tolerance that works: which links can give up tolerance for the biggest cost saving while the stack still passes.
Greedy, one IT grade at a time — every trial loosening is re-verified against tolerance_stackup's seeded Monte-Carlo cpk before it is committed, so nothing it suggests can fail the spec. Candidates are ranked by the tolerance_cost_check curve, so the tightest (most expensive) links get opened first. Loosening preserves each link's MEAN, so the stack's nominal does not move.
Two honest refusals: a chain that does not already meet target_cpk returns
ok=False (there is no margin to give away — tighten or re-spec, do not loosen),
and an already-loosest chain returns steps=[] with saving=0 rather than
inventing a saving. stopped says which: 'no_further_move' | 'max_steps'
(re-run on the returned chain to continue) | 'no_margin'.
chain / handle / axis / default_tol / general as in
tolerance_cost_check; spec_min/spec_max default to the chain's own
worst-case bounds. Returns {ok, steps:[{link, index, from_it, to_it,
from_band_mm, to_band_mm, cost_before, cost_after, saving, cpk}], stopped,
chain (the loosened scheme), cost_index_before, cost_index_after, saving,
saving_pct, cpk_before, cpk_after, target_cpk, spec, note, fidelity, band_pct,
basis}.
Inputs: axis, seed, chain, handle, +11 more.
sweepSweepA
Sweep a profile sketch along a spine sketch (additive pipe).
profile: handle of the cross-section sketch. spine: handle of the path sketch (in the same body). mode: 'Standard' | 'Frenet' | 'Auxiliary' | 'Binormal'. transition: 'Transformed' | 'Right corner' | 'Round corner'.
Inputs: spine (required), profile (required), mode, name, +2 more.
thermal_composite_wallThermal Composite WallARead-only
Exact series thermal-resistance network of a plane composite wall (NO solver)
— the classic overall-U calculation and the conjugate-heat-transfer family's
closed-form oracle. layers is the in→out list of solid layers, each
{thickness_mm, k | material} (k in W/m·K, or a Materials-DB name); h_in/h_out
are optional convection film coefficients (W/m²K). Per unit area
R = 1/h_in + Σ tᵢ/kᵢ + 1/h_out, U = 1/R, q = U·(t_in − t_out), and every
surface/interface temperature follows exactly.
Returns {u_w_m2k, r_total_m2k_w, q_w_m2, q_w, layer_resistances_m2k_w, interface_temps_c (inner surface → outer surface), t_in_c, t_out_c, area_m2}.
Inputs: layers (required), t_in_c (required), t_out_c (required), h_in, +3 more.
thermal_lumpedThermal LumpedARead-only
Lumped first-order transient warm-up (no mesh). ΔT_ss = P/(h·A),
τ = m·c_p/(h·A), T(t) = T_amb + ΔT_ss·(1−e^(−t/τ)). c_p is an explicit
value/quantity-string ('900 J/kg/K') or read from material. Get h_conv from
the h_estimate correlation screen rather than guessing. With duration_s
the temperature + fraction-of-steady reached are returned. A radiation screen
flags when the steady-state radiative HTC exceeds h_conv. Returns
{t_ambient_c, delta_t_steady_k, t_steady_c, time_constant_s, t_final_c,
reached_steady_pct, h_rad_w_m2k, radiation_significant}.
Inputs: h_conv (required), mass_g (required), power_w (required), area_mm2 (required), +6 more.
thermal_radiation_submitThermal Radiation SubmitADestructive
Diffuse-gray radiation FEM via Elmer, asynchronous — the radiation sibling of
thermal_transient_submit. Requires ElmerSolver + the ViewFactors binary (apt
elmerfem-csc / conda); when absent this returns {ok:false, reason, install}
rather than raising. Two modes:
Build the two-plate enclosure case (no case prep): pass
t1_c,t2_c(°C) and the two surface emissivitiesemissivity_1/emissivity_2(default 0.8). The handler writes a 2-D pair of parallel plates radiating across an unmeshed vacuum gap, runs ViewFactors then ElmerSolver, and extracts the net radiative exchange — directly gated against the exact two infinite parallel plates oracle q = σ(T₁⁴−T₂⁴)/(1/ε₁+1/ε₂−1) (oracle_ratio≈ 1). Mesh/geometry knobs:width_m,gap_m,plate_thickness_m,n_x,k_plate.Run a prepared
case_dircontaining its.sif+ mesh (ViewFactors is run first when no factor file is present).
Returns the degradation dict, or {job_id, status, cache_hit}; poll job_result for {ok, returncode, solver, case_dir, stdout_tail} plus, for the plate case, {flux_w_m2, q_net_w, two_plate_flux_w_m2, oracle_ratio, t1_c, t2_c, emissivity_1, emissivity_2} (or {scalars_final} for a prepared case).
Inputs: n_x, sif, t1_c, t2_c, +8 more.
thermal_transient_1dThermal Transient 1DARead-only
Analytic 1-D plane-wall transient conduction (one-term Heisler series), valid
for Fourier ≳ 0.2 — the closed-form transient the Elmer thermal_transient solve is
gated against, and the distributed (spatial-gradient) answer the lumped screen
only approximates. A wall of half-thickness L cools/heats toward ambient by surface
convection: Bi = h·L/k, Fo = α·t/L², α = k/(ρ·cₚ). Pass alpha_m2_s, or k+rho+cp,
or a material (Materials DB: thermal_conductivity/Density/specific_heat); get
h_conv from the h_estimate correlation screen rather than guessing.
As Bi→0 the body is isothermal and this collapses to the lumped exponential exp(−t/τ) (cross-checked via t_center_lumped_c / lumped_agrees). Returns {biot, fourier, eigenvalue_1, c1, t_center_c, t_surface_c, t_center_lumped_c, time_constant_s, one_term_valid, lumped_agrees}.
Inputs: h_conv (required), duration_s (required), half_thickness_mm (required), k, +7 more.
thermal_transient_submitThermal Transient SubmitADestructive
Transient thermal FEM via Elmer, asynchronous. Requires ElmerSolver (apt
elmerfem-csc / conda); when absent this returns {ok:false, reason, install}
rather than raising. Three modes:
Build the analytic-slab case (no solver case prep needed): pass the plane-wall transient —
half_thickness_mm,h_conv(W/m²K),duration_s, and eitherk+rho+cp(SI) or amaterialname, with optionalt_initial_c/t_ambient_cand mesh/step countsn_elements/n_steps. The handler writes the 1-D conduction case (symmetry at the centre, convection at the surface), runs ElmerSolver, and returns the centre/surface temperatures — the same plane-wall BVPthermal_transient_1dsolves analytically, so the two are directly comparable (the kickoff's relative gate).Solve a real FreeCAD solid — the geometry bridge: pass a
bodyhandle plusconvection_faces(1-based indices into the solid's faces; those faces get theh_conv/t_ambient_cconvective BC, every other face is adiabatic), the physics (h_conv,duration_s,k+rho+cpormaterial), and an optionalchar_length_mmGmsh element size andelement_order('1st'|'2nd'). The solid is Gmsh-meshed and solved as a true 3-D body (ElmerGrid + ElmerSolver); the result's {t_max_c, t_min_c} are the interior/convective-surface temperatures (for a slab-like body, directly gateable against thermal_transient_1d). Preferelement_order='2nd'for a sharp transient — quadratic tets resolve the wall gradient accurately even on a coarse mesh.Run a prepared
case_dircontaining its own.sif+ mesh.
Returns the degradation dict, or {job_id, status, cache_hit}; poll job_result for {ok, returncode, solver, case_dir, stdout_tail} plus, for the slab case, {t_center_c, t_surface_c, n_steps_written}, for a body {t_max_c, t_min_c, nodes, tets} (or {scalars_final} for a prepared case).
Inputs: k, cp, rho, sif, +14 more.
thicknessThicknessB
Hollow out a solid into a shell.
base: handle of the body's tip feature (the solid to hollow). open_faces: list of {handle, face: tag|'FaceN'} that become the shell's openings. thickness: wall thickness (mm). reversed: True (default) grows the wall INWARD into the solid (the natural "hollow this part" interpretation). False grows outward. join: 'Arc' | 'Intersection'. mode: 'Skin' | 'Pipe' | 'RectoVerso'.
Inputs: base (required), open_faces (required), join, mode, +4 more.
tolerance_cost_checkTolerance Cost CheckARead-only
Price a tolerance scheme against the process that has to hold it — the missing link between tolerance_stackup ("what tolerance works") and cost_estimate ("what does it cost").
Per toleranced dimension: its ISO 286 IT grade as a FLOAT (a band between IT6
and IT7 reports 6.4, not 7), the cheapest machining operation that holds that
grade naturally (drilling ~IT11, milling ~IT10, turning ~IT9, reaming ~IT7,
grinding ~IT6), a relative cost index normalised to 1.0 at process's natural
capability, and a verdict: 'ok', 'in_process_tightening' (tighter but reachable
in the same operation), or 'needs_secondary_operation' — the flag, meaning the
part silently acquired an operation nobody costed. pass is false when any link
flags. Cost roughly doubles every 1.5 IT grades tightened below natural
capability; above it only inspection/scrap falls. total_cost_index is the sum,
so two tolerance SCHEMES over the same chain compare directly.
chain is the same [{name, nominal, plus, minus | tol}] tolerance_stackup
takes; instead pass a live handle (+ axis, default_tol, general) and the
chain is derived off the solid the same way. process: cnc | injection |
casting | sheet | fdm | drilling | milling | turning | boring | reaming |
grinding | honing | lapping.
fidelity='correlation', band_pct=50 — the RATIOS are defensible, the absolute index is dimensionless and is not money. Returns {process, links:[{name, nominal_mm, band_mm, it_grade, cost_index, verdict, cheapest_operation, natural_it, note}], n_links, total_cost_index, mean_cost_index, flagged, pass, fidelity, band_pct, basis, escalate_to}.
Inputs: axis, chain, handle, general, +3 more.
tolerance_stackupTolerance StackupARead-only
Stack a dimension chain. Each chain entry is {name, nominal, plus, minus} with plus/minus the signed upper/lower deviations (plus>=minus; symmetric shorthand {nominal, tol}); add direction:-1 for a subtractive/gap link. method: worstcase | rss | montecarlo (each adds a deeper block). Half-bands are read as 3-sigma; cpk/pct_in_spec use spec_min/spec_max if given, else the worst-case bounds. Returns {nominal, worstcase:{min,max,spread}, rss:{sigma,min_3s,max_3s}, montecarlo:{mean,std,cpk,pct_in_spec,spec}}.
Instead of a hand-built chain, pass a live handle (+ axis, '+z'/'-x'/… or
[x,y,z]) and the chain is derived off the solid: planar step faces
perpendicular to the axis become consecutive station-to-station links (the
stack a height gauge reads off a stepped part). Per-link tolerance:
default_tol (± mm), else the ISO 2768-1 general class ('f'|'m'|'c'|'v',
default 'm' — the drawing-note default for untoleranced dimensions). The
result then echoes the derived chain (+ axis, n_step_faces).
seed fixes the montecarlo draw (12345 default) so the same chain returns the
same cpk/pct_in_spec run to run — that determinism is a contract, so change it
only to check a result is not an artefact of one draw.
Inputs: axis, seed, chain, handle, +7 more.
topology_optimize_submitTopology Optimize SubmitA
Minimum-compliance topology optimization (in-house SIMP; NO external solver),
asynchronous because each iteration solves an FE system. Optimizes a 2-D
rectangular design domain (nelx×nely unit cells) — or, when nelz is set, a
3-D nelx×nely×nelz grid of trilinear hexahedra — to the stiffest layout that
holds Σdensity = keep_fraction (the Optimality-Criteria update holds it exactly);
penal is the SIMP penalty (≈3), rmin the cone filter radius. Default BCs
(both): the whole left face clamped + a unit downward load at the right-face
centre. 2-D overrides: fixed_dofs / load=[dof_index, value]. 3-D overrides:
loads=[[i,j,k,axis,value],...] point loads at node grid coords (axis 'x'|'y'|'z'),
fixed_nodes=[[i,j,k],...] clamped nodes, and keep_out/keep_in lists of
half-open element-index boxes [i0,i1,j0,j1,k0,k1] forced void / forced solid
(keep-out regions and must-keep pads).
Returns immediately {job_id, status, cache_hit}; poll job_result for {density
(2-D: nely×nelx grid; 3-D: nelz×nely×nelx voxel field, density[k][j][i] with j=0
at the bottom — this IS geometry), mass_fraction (==keep_fraction), compliance,
compliance_initial, iterations, converged, gray_fraction, solver (3-D: which
linear-solve backend ran)}. Threshold + voxel→solid back in the modeller with
topology_to_solid, then gate with mass_properties (mass ≤
keep_fraction·original) and interference_check vs keep-outs.
Inputs: tol, load, nelx, nely, +11 more.
topology_to_solidTopology to SolidA
Reconstruct a FreeCAD solid from a topology-optimization density field — the
modeller-side close of the loop opened by topology_optimize_submit, whose
density this consumes. 2-D (nely×nelx grid): thresholds (a cell is solid when
density ≥ threshold), run-length-merges each row into solid spans, tiles each
span as a cell_mm box extruded thickness_mm in Z (row 0 at the top). 3-D (a
nelz×nely×nelx voxel field from the nelz mode): greedy-merges solid voxels into
maximal boxes at (i·cx, j·cy, k·cz) — j=0 at the bottom, thickness_mm ignored.
Fuses into one static Part::Feature. cell_mm is a scalar or [cx, cy(, cz)] mm;
thickness_mm defaults to the smaller cell edge; placement is an optional
[x, y, z] mm origin offset; name names the object. Runs synchronously (it builds
geometry — no jobs.py poll).
Returns {handle, name, volume (mm³), solid_cells, total_cells, mass_fraction (== solid_cells/total_cells — must be ≤ keep_fraction within one cell), n_solids (disjoint bodies; >1 means a split load path), threshold, nelx, nely, nelz (None for 2-D), bbox_mm}. Gate it with mass_properties (mass ≤ keep_fraction·original) and interference_check against keep-out regions, per SIMULATION_EXAMPLES §5.
Inputs: density (required), name, cell_mm, placement, +3 more.
transaction_abortTransaction AbortADestructive
Roll back the most recent open transaction. Implementation note:
FreeCAD 1.1 headless abortTransaction is unreliable, so the worker
commits then undoes — net effect is a clean rollback.
Inputs: workspace.
transaction_commitTransaction CommitA
Commit the most recent open transaction; changes are kept.
Inputs: workspace.
transaction_openTransaction OpenA
Begin an undoable transaction on the active document. Pair with transaction_commit (keep the changes) or transaction_abort (roll back). Transactions nest — the most-recent open is committed/aborted first.
Inputs: label, workspace.
transformTransformADestructive
Move and/or rotate an object in place (no new handle) — use instead of editing Placement via set_property.
translate: [x, y, z] mm. rotate_axis: [x, y, z], any length (default Z); angle: DEGREES about it, around the object's local origin. relative: True composes onto the current placement; False sets it absolutely.
Returns {handle, name, placement: {base [x,y,z] mm, axis, angle_deg}}.
Inputs: handle (required), angle, relative, translate, +2 more.
use_workspaceUse WorkspaceA
Claim (creating if needed) an isolated workspace and make it the target of subsequent tool calls. Each workspace is its own freecadcmd process with its own ActiveDocument and handle registry — handles do NOT cross workspaces.
The selection is SERVER-WIDE: it retargets every client sharing this server,
including other subagents of the same session. Concurrent agents must instead
pass workspace=<unique name> on every tool call, which pins just that call
and leaves this selection alone.
The pool is capped (ANKUSDRIVE_MAX_WORKSPACES, default 4) and idle workspaces are reaped (ANKUSDRIVE_WORKSPACE_IDLE_S, default 900s); claiming a workspace beyond a full pool raises — close an idle one first. Pass "default" to return to the baseline single-agent workspace. Returns {workspace: , freecad: [...], workspaces: [names]}.
Inputs: name (required), workspace.
validate_manifestValidate ManifestARead-only
Validate a manifest WITHOUT building it (the cheap front door, RFC §11.7).
Structural + cross-reference checks a JSON shape can't enforce: every component
has exactly one of file/manifest/library; a library carries a tool; every
instance references a known component; every mate/check references a known
instance; a present schema is the known version ("ankusdrive.manifest/1").
manifest: path to the manifest JSON.
Returns {ok, problems, schema, manifest_hash} — ok is True iff problems is empty; manifest_hash fingerprints the contract content (what the lockfile records so a stale contract is detectable). Run this before merge_assembly to reject a malformed contract before any geometry is built.
Inputs: manifest (required), workspace.
verify_contractVerify ContractARead-only
Build-time self-check of a component against its manifest slice (RFC §11.3): a builder calls this on its OWN part before save, so a contract violation is caught locally and cheaply instead of after a fan-in merge (build → merge → gate-fail → rebuild becomes build → self-check → fix). Never raises on a failing check (a failure is a passed=False row), so it is safe to call in a loop. Inspection only; mutates nothing.
handle: the component's shaped object. contract: the component's slice — all keys optional, give at least one: envelope {min:[x,y,z], max:[x,y,z]} the part's LOCAL bbox must fit in it. interfaces {name: {origin:[x,y,z], z_axis?:[x,y,z], tol_mm?, angle_tol_deg?}} each named frame must be PUBLISHED (publish_interface) and within tolerance of the contracted origin (and axis, if z_axis given) — catches "forgot to publish" / "published in the wrong place". features [ {kind, ...} ] per-feature self-checks: {kind:"gear", module_mm, teeth, internal?, tol_mm?} {kind:"bore", diameter_mm, tol_mm?} {kind:"extent", axis:"x"|"y"|"z", length_mm, tol_mm?} intent bool also run verify_intent.
Returns {handle, ok, results:[{check, passed, detail}]} — ok True iff every check passed; check names are envelope / interface: / feature: / intent.
Inputs: handle (required), contract (required), workspace.
verify_featureVerify FeatureARead-only
Compare a feature's actual volume change against an expected signed delta. Run after each subtractive/additive operation to catch silent failures — Pocket on a curved surface that under-cut, Hole that drilled outside the body, Cut whose Tool didn't intersect the Base.
handle: PartDesign feature (Pad/Pocket/Hole/Revolve/etc.) or Part::Cut. expected_delta_mm3: SIGNED expected change. Subtractive → negative, additive → positive. Wrong sign is its own useful error. tolerance: relative tolerance (default 0.05 = 5%). abs_tolerance: absolute mm³ fallback for tiny expected magnitudes (default 0.01). Pass if EITHER tolerance is satisfied.
Returns {passed, message, actual_delta_mm3, expected_delta_mm3, ratio,
previous_volume_mm3, current_volume_mm3, handle, name}. Does NOT raise
on mismatch — inspect passed to decide whether to abort.
Inputs: handle (required), expected_delta_mm3 (required), tolerance, workspace, +1 more.
verify_intentVerify IntentARead-only
Re-run every invariant declared with declare_intent — the regression gate to run after each edit. Composes check_shape / check_airtight_path / face-role resolution; never raises on a failing invariant (a failure is a passed=False row), so it is safe to call in a loop. Inspection only; mutates nothing.
handle: the part (must have a declared intent contract).
Returns a dict: handle (str) ok (bool) True iff every declared invariant passed results (list) one {invariant, passed, detail} per declared invariant — invariant in {watertight, airtight_path, required_faces}, detail a human-readable summary of what was measured
Inputs: handle (required), workspace.
verify_performanceVerify PerformanceADestructive
Prove (or fail to prove) every requirement declared with declare_performance — the step that turns "a solver printed 0.29" into a claim with a band and a provenance.
A verdict has THREE states. pass and fail each require the measurement's whole
uncertainty band to sit on one side of the limit; a band that straddles it is
indeterminate, meaning "escalate", not "probably fine". A correlation reading
Cd = 0.28 ± 10 % against a limit of 0.30 spans 0.252–0.308 and has NOT shown the
part passes — collapsing that to a pass is how a spec silently goes unmet.
tier picks the evidence:
'screen'— each requirement's cheap estimator only. Milliseconds, no solver.'solver'— the real solve for every requirement.'auto'(default) — screen first, escalate only what the screen could not decide or what declaresfidelity_floor: 'solver'. This is the ladder that keeps a design loop cheap: cheap measurements eliminate candidates, solves confirm survivors.
Trust is part of the measurement, not a footnote: trust: {converged: true} or a
band_max_pct cap makes an unconverged (or insufficiently mesh-converged) solve
come back indeterminate with the reason, never pass.
Solver-tier measurements are asynchronous, so this returns EITHER the finished verdict (screen-only, or everything already decided) or {job_id, status, pending, results} — poll job_result for the completed verdict. Never raises on a failing requirement; a failure is a row.
Every verdict is also RECORDED on the part, stamped with a geometry signature of
the shape it measured (#261). That record is what merge_assembly,
substitutability_check and component_contract_check consult, since a gate has to
answer synchronously and this may not have: an in-flight solve records rows the
gates read as unverified, and editing the part invalidates the signature so they
read stale — never a pass on either path.
Returns {handle, tier, ok, n_requirements, passed, failed, indeterminate, escalate, results: [{name, tier, metric, state, measured, limit, band_pct, worst_case, best_case, margin, margin_pct, detail, trust_reasons?, screen?, job_id?}]}.
Inputs: handle (required), tier, workspace.
versionVersionBRead-only
Return FreeCAD and bundled Python versions from the worker.
Inputs: workspace.
waveguide_cutoffWaveguide CutoffARead-only
Exact rectangular-waveguide cutoff frequency (NO solver) — the closed-form
twin the openEMS FDTD full-wave solve (em_fullwave_submit) is gated against.
Broad wall a_mm, narrow wall b_mm (default a/2, WR convention). mode is
'TE'/'TM'. f_c(m,n) = (c/2√εᵣ)·√((m/a)²+(n/b)²); dominant TE10
reduces to the EXACT f_c = c/(2a√εᵣ). Below f_c the guide is evanescent
(axial β imaginary, nothing transmits), above it propagates with guided
wavelength λ_g = 2π/β. With a probe freq_ghz the regime (propagating /
evanescent), k, β, λ_g and (below cutoff) the exact attenuation
α = √(k_c²−k²) are returned — α is what the FDTD solve's field probes are gated
against (alpha_ratio).
Returns {mode, m, n, a_mm, b_mm, eps_r, cutoff_hz, cutoff_ghz, kc_per_m, next_mode_cutoff_ghz, single_mode_band_ghz, probe_freq_ghz, regime, k_per_m, beta_per_m, alpha_per_m, guided_wavelength_mm, fidelity, band_pct, valid_range_ok, warnings, escalate_to}.
Inputs: a_mm (required), b_mm, mode, eps_r, +2 more.
wear_estimateWear EstimateARead-only
Estimate sliding wear (Archard): V = k·F·s/H. k (wear_coef) is empirical — pass it, or it's looked up by the material_pair's category pair (order-of- magnitude). H (hardness_mpa) defaults to Tabor 3·σ_y of the softer member. With apparent_area_mm2 a mean depth is reported and gated by max_depth_mm. Returns {wear_coef, hardness_mpa, volume_loss_mm3, depth_loss_mm, coef_basis, hardness_basis, pass}.
Inputs: load_n (required), sliding_dist_m (required), wear_coef, workspace, +4 more.
weld_groupWeld GroupARead-only
Rate a planar fillet-weld group by Blodgett's treat-weld-as-a-line method.
segments=[((x1,y1),(x2,y2)),...] (mm); an in-plane force_n=[Fx,Fy] at
load_point_mm=[px,py] gives direct shear f=F/L plus torsional f=Tr/J from the
eccentric moment about the weld centroid, added vectorially at the worst end.
required_leg = f_r/(0.707allowable); given leg_mm, throat stress f_r/(0.707*leg)
is checked vs allowable. Returns {weld_length_mm, centroid_mm, Ix_mm3, Iy_mm3,
J_mm3, direct_shear_n_per_mm, max_shear_n_per_mm, worst_point_mm,
required_leg_mm, throat_stress_mpa?, shear_sf?, pass}. weld_type ('fillet'
default) labels the group and selects the throat convention.
Inputs: force_n (required), segments (required), load_point_mm (required), leg_mm, +3 more.
where_usedWhere UsedARead-only
Where-used / impact analysis (issue #142, C3; MULTI_AGENT.md §9): traverse the
lockfile depends_on graph and report every parent that CONSUMES an item — the
blast radius, so exactly those parents re-dispatch when the item changes. The
lockfile records edges consumer -> consumed (lid depends_on housing); where-used
is the reverse reachability of the item (everything that reaches it).
lockfile: path to the lockfile (the JSON assembly_lock wrote). item: the item / component id to query. direct: if true, also surface only the immediate mates separately.
Returns {item, where_used (the full transitive blast radius), direct (the §9 immediate-neighbour layer)} — an unknown item fails loudly, never a silent empty set.
Inputs: item (required), lockfile (required), direct, workspace.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 286 tools
Most families are individually well-signposted, but at 286 tools there are repeated near-pairs an agent must disambiguate by reading long prose: fillet_edges vs partdesign_fillet, chamfer_edges vs partdesign_chamfer, shell_solid vs thickness, moldability_screen vs moldability_check vs optics_moldability_check, measure_distance vs min_clearance vs interference_check, and five overlapping catalog/designation tools. The descriptions do explain the differences (Part vs PartDesign workbench, screen vs solve), but selection depends on long-text reading rather than obvious naming.
Names are uniformly snake_case and overwhelmingly verb_noun (add_fastener, list_faces, measure_angle, verify_intent, declare_performance), with a stable family prefix convention (fem_*, cfd_*, sheet_*, optics_*, lifecycle_*, *_submit). Minor deviations exist where geometry primitives are bare verbs or nouns (pad, pocket, hole, revolve, loft, sweep, recipe, transform), which breaks the pattern slightly but stays readable.
286 tools is far beyond any reasonable single-server surface; even generously grouped (modelling, measurement, FEM, CFD, acoustics, optics, DEM, molding, sheet metal, PLM/lifecycle, recipes), the count is an extreme mismatch for one coherent toolset. An agent cannot hold this surface in working context, and many tools are niche oracles that will rarely be selected correctly.
Coverage is exhaustive across the full design lifecycle: parametric modelling, booleans/blends, measurement and inspection, assemblies with gates and lockfiles, catalog/buyability, FEM/CFD/thermal/acoustic/EM/molding/optics/DEM solvers each with analytic oracles, drawings and GD&T, cost/DFM, sheet metal, and PLM revision/lifecycle. Every operation appears to have its counterpart (create/verify/lock/export), leaving no obvious dead ends.
Maintenance
Related MCP Connectors
Parametric 3D CAD for AI agents: build print-ready parts, check them, export STL, 3MF or STEP.
CAD in ChatGPT and Claude: editable parts, STEP/STL/drawings, in-chat 3D, automatic checks.
214 tools for AI agents over one MCP endpoint: image, video, audio, 3D, data, storage, mail, GPUs.
Text-to-CAD: build exact STEP or 3MF/STL on the server, each stated requirement measured.
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceEnables AI agents to perform CAD operations through FreeCAD, including part conversion, 3D printing slicing, fluid simulation, and structural analysis, with a web dashboard for human interaction.33MIT
- AlicenseCqualityDmaintenanceAllows AI agents like Claude to create, edit, and query 3D models in FreeCAD through natural language.579MIT
- AlicenseNot gradedqualityDmaintenanceEnables LLMs to safely generate parametric CAD parts (STEP/STL) using verified templates and FreeCAD, with validation and assembly support.1MIT
- AlicenseBqualityBmaintenanceEnables coding agents to turn mechanical requirements into validated FreeCAD models with deterministic sizing from published standards, standard-part provenance, and automatic geometry validation.131Apache 2.0