ping
Ping
Check that the FreeCAD worker is alive. Returns 'pong' on success.
workspace
io.github.gchen19Published by GitHub account gchen19v0.5.7Updated Oct 30, 2026
Drive FreeCAD from an agent: CAD modeling, drawings, FEM, CFD, EM, and multiphysics simulation.
Drives a local FreeCAD 1.1 install so an assistant can build CAD models, run FEM/CFD simulations, make drawings and check designs.
Desktop only via STDIO. STDIO servers start a local process, so they need the SourceWeft desktop host.
Follow the launch instructions in the repository.
[AnkusDrive — align generative intent with the CAD kernel]
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.
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:
ankusdrive run script.py, ankusdrive box --w 10 --d 20 --h 5 -o part.FCStd) for scripts, CI, and quick iteration.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).freecadcmd binary is auto-discovered per-OS (macOS .app bundle, Linux /usr/bin etc., Windows C:\Program Files\FreeCAD 1.1\bin\freecadcmd.exe — version-globbed); override via $ANKUSDRIVE_FREECADCMD or rely on PATH. Run ankusdrive doctor to see exactly what resolved.ccx (CalculiX), and gmsh already ship inside every FreeCAD install — the macOS .app, the Linux package, and the Windows bin\ — so core CAD + structural FEM work on all three with no extra install.Pillow and numpy; both are installed by AnkusDrive as regular pip deps.export_drawing) renders inside FreeCAD's bundled Python, so it needs reportlab + svglib installed there — see Drawing export (PDF/SVG). DXF export and everything else leave FreeCAD's Python untouched.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.
On 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.
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.
The 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.
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:
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):
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:
That 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:
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.
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:
It 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.
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:
So a stock installer on any of the three needs no configuration. For a non-default install, point AnkusDrive at the binary directly:
ankusdrive 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.
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:
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.
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):
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:
Other 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.
ANKUSDRIVE_TOOLSETS, e.g. ANKUSDRIVE_TOOLSETS=drawings,fem,simulation, or
toolsets = "..." in config.toml.core, drawings and fem are 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):
false.When it's off, the tool isn't offered at all, and setup_status says how to enable it.
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):
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.
Two optics engines sit behind the MCP surface, in two licensing/runtime lanes:
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):
Key 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).
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, boolean cut/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, Placement for positioningFEM (ObjectsFem + femtools):
ObjectsFem.makeAnalysis(doc, "Analysis") — containermakeSolverCalculixCcxTools / makeSolverElmer — solver objects with tunables (GeometricalNonlinearity, ThermoMechSteadyState, …)makeMaterialSolid — assign YoungsModulus, PoissonRatio, DensitymakeConstraintFixed, makeConstraintForce, makeConstraintPressure, makeConstraintDisplacement, contact/tie/spring, thermalmakeMeshGmsh + femmesh.gmshtools.GmshTools(...).create_mesh() (or Netgen)femtools.ccxtools.FemToolsCcx().run()analysis.Group for Fem::FemResultObject; read .DisplacementVectors, stress fieldsHeadless invocation:
freecadcmd script.py — runs script then exitsfreecadcmd with no args — interactive Python REPL (what the worker will drive)--console, -M <moddir>, -P <pypath>, --pass <args>, FreeCAD.ConfigGet(...) for env infoFreeCADGui is not available headless — keep design logic in App/Part/Fem onlyAnkusDrive 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.
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.
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.
For the long tail of "I just need to tweak this one property" without a dedicated tool:
get_object(handle) — dump every entry in obj.PropertiesList with
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).
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.
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.
session_transcript() returns the session so far as a script:
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.
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:
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.
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:
Every 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:
or, 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:
The CLI is not the agent surface — it's a human-debugging + transport tool. Seven subcommands:
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.
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:
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 an
assembly_lock / assembly_lock_check contract lockfile. These are real
MCP tools usable by any host.orchestration/ is the host-side reference coordinator — explicitly
not part of the ankusdrive package. Given a free-text brief it
decomposes 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.
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.
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 typed
units layer rejects dimensionally-wrong inputs at the
door ("5 N" for a length is an error, not a silent mis-scale).family_materialize expands a row × column design
table into a set of variants deterministically, running the recipe per row and
allocating part numbers in table order.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.eco_create) are first-class change records;
where_used / change_impact compute blast radius over the dependency graph
before you commit; baseline_create / baseline_verify pin reproducible
snapshots.get_interface,
nema17_face@1-style named/versioned types) plus a Liskov
substitutability_check gate enforce Form/Fit/Function compatibility as code,
so a swapped part is verified to actually mate.scaffold_project + project_validate /
project_check_references promote the multi-agent directory convention to a
first-class project.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.
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).
freecadcmd worker, newline-JSON over stdio with stdio hygiene (FreeCAD C++ chatter redirected off the protocol fd).ping, version, box, cylinder, export, run, mcp, fem cantilever, plus top-level --version.*_submit/job_*), the materials/fluids corpora, Design-for-X / manufacturing checks, the design-control (PLM) layer, and transactions.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).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 and docs/MULTI_AGENT.md.direction='into_body'|'away_from_body' and through='wall'|'body' on pocket/hole (ray-cast wall depth handles hollow shells correctly); intended_for='print'|'machine'|'drawing' on hole drives ModelThread; verify_feature diffs actual-vs-expected volume change to catch silent failures; visibility hygiene at save hides consumed inputs; register_handle + run_script auto_register close the escape-hatch one-way trapdoor; list_thread_options surfaces the coupled ThreadType/ThreadSize enums dynamically; revolve has an OCCT pre-check that flags axis-coincident edges with an actionable error.list_faces / list_edges / query_faces / resolve_* produce stable geometric tags that survive edits; FEM constraints take tags directly.ankusdrive/render.py) with per-pixel z-buffer (render_view / render_views return PNGs as MCP ImageContent), plus photoreal render_photoreal via the FreeCAD Render addon + an external renderer (POV-Ray / LuxCore / Appleseed / Cycles / OSPRay / PBRT). Support matrix, install, and limitations: docs/RENDERING.md.ankusdrive/solvers.py and 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 via material_* and fluid_props (mechanical-property / molding / CoolProp corpora).docs/DESIGN_HIERARCHY.md. See Designs, not just parts.tests/run_all.sh (Linux/macOS) or tests/run_all.ps1 (Windows — single-interpreter, skips the Linux-only solver families; see docs/WINDOWS.md). Reliability harness (Layer A classification, B diff-detection, C agent-loop closure) is gated behind RUN_RELIABILITY=1; see tests/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.
list_documents / set_active_document / close_document), and every long-running solve — CalculiX included, via fem_run_submit — can run off the channel through the *_submit + job_* pattern. The synchronous fem_run remains 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.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.
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.
Source: README.md at commit bcbfab3
Ping
Check that the FreeCAD worker is alive. Returns 'pong' on success.
workspaceRestart Worker
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: <name>, freecad: [...]}.
workspaceUse Workspace
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: <name>, freecad: [...], workspaces: [names]}.
namerequiredworkspaceList Workspaces
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: [...]}.
workspaceClose Workspace
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: <bool>, workspace: <name>, current: <name>, selected: <name>}.
namerequiredworkspaceSession Transcript
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]`.
workspaceprovenancecheckpointsprune_abortedinclude_read_onlyJournal Export
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, ...}.
sessionworkspaceprovenanceVersion
Return FreeCAD and bundled Python versions from the worker.
workspaceNew Document
Create a new FreeCAD document and make it active. Returns {doc: <name>}.
nameworkspaceOpen Document
Open an existing .FCStd file, make it active. Returns doc name and object list.
pathrequiredworkspaceSave Document
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).
pathrequiredworkspaceattach_provenancevisibility_hygieneList Objects
List objects in the active document. Returns [{name, type, label}, ...].
workspaceAdd Primitive
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.
dhrwkindrequirednameplacementworkspaceAdd Gear
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.
nameteethrequiredheightmodulerequiredexternalplacementworkspacepressure_angleAdd Rack
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.25*module, tooth height = 2.25*module, 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 = module*pi), module, teeth, tooth_height (2.25*module mm), length (teeth*module*pi 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.
nameteethrequiredwidthheightmodulerequiredplacementworkspacepressure_angleAdd Sprocket
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).
nameteethrequiredheightplacementworkspacechain_pitchrequiredroller_diameterrequiredAdd Pulley
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.
nameteethrequiredwidthrequiredheightflangedplacementworkspacebelt_pitchrequiredAdd Spring
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 = G*d^4 / (8*D^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).
kindnamecoilsrequiredplacementworkspacefree_lengthrequiredwire_diameterrequiredouter_diameterrequiredAdd Fastener
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 `order
kindrequirednamesizerequiredgradelengthplacementworkspaceAdd Bearing
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).
borenamesealswidthplacementworkspacedesignationouter_diameterO-Ring Groove
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_section*0.75 (~25% squeeze, clamped to a 20-30% band), groove_width = cross_section*1.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_ou
cutfacenamehandlecompoundworkspacegland_typecross_sectionrequiredinner_diameterChamfer Edges
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.
namesizeedgesrequiredhandlerequiredper_edgeworkspaceallow_partialShell Solid
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.
namefacesrequiredhandlerequiredthicknessrequiredworkspaceAdd Thread
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,
namegradepitchrequiredlengthrequiredstartsdiameterrequiredinternalplacementworkspaceEngrave Text
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}.
facerequiredfontmodenamesizetextrequireddepthhandlerequiredpositionworkspaceAdd Rib
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).
bodyrequirednamesketchrequiredmidplanereversedthicknessrequiredworkspaceTransform
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}}.
anglehandlerequiredrelativetranslateworkspacerotate_axisScale Shape
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 sx*sy*sz.
namecenterfactorrequiredhandlerequiredworkspaceCopy Shape
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 '<SourceName>_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.
namehandlerequiredplacementworkspaceMeasure Distance
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.
arequiredbrequireda_refb_refworkspaceMeasure Angle
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.
arequiredbrequireda_refrequiredb_refrequiredworkspaceBounding Box
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.
tighthandlerequiredorientedworkspacedeflectionMin Clearance
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").
arequiredbrequiredworkspaceCheck Shape
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)}.
handlerequiredworkspaceCheck Airtight Path
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
inletrequiredhandlerequiredoutletrequiredpad_mmworkspacemin_aperture_mm2Classify Face Sides
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
handlerequiredworkspaceseal_portsSection View
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)}.
nameplanehandlerequiredoffsetworkspaceemit_profileList Faces
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.
handlerequiredworkspaceList Edges
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.
handlerequiredworkspaceQuery Faces
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.
handlerequiredpredicaterequiredworkspaceResolve Face
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).
tagrequiredhandlerequiredworkspaceResolve Edge
Resolve an edge tag to the current EdgeN index. Errors on miss or ambiguity.
tagrequiredhandlerequiredworkspaceMake Body
Create a PartDesign Body. Subsequent sketches/features go inside it. Returns {handle, name}.
nameworkspaceMake Datum Plane
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.
basebodyrequirednameoffsetworkspaceMake Sketch
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}.
bodyrequirednameplaneworkspaceAdd Sketch Geometry
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.
itemsrequiredsketchrequiredworkspaceAdd Sketch Constraint
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.
refsrequiredtyperequiredvaluesketchrequiredworkspaceClose Sketch
Recompute and report DOF status. Returns {geometry_count, constraint_count, open_vertices, fully_constrained}.
sketchrequiredworkspacePad
Pad a sketch by `length` mm. symmetric=True extrudes both directions.
namelengthsketchrequiredreversedsymmetricworkspaceSubtract 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.
namelengthsketchrequiredstrictthroughreverseddirectionworkspacethrough_allRevolve
Revolve a sketch around a body origin axis ('X'|'Y'|'Z') by `angle` deg.
axisnameanglesketchrequiredreversedworkspacePartDesign Fillet
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.
nameedgesrequiredradiusfeaturerequiredper_edgeworkspaceallow_partialPartDesign Chamfer
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.
namesizeedgesrequiredfeaturerequiredper_edgeworkspaceallow_partialHole
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.
namedepthsketchrequiredstrictthroughcut_typediameterreversedthreadedcut_depthdirectionworkspace+6 moreList Hole Thread Options
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: [...]}.
workspacethread_typeLinear Pattern
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.
namelengthfeaturerequiredreverseddirectionworkspaceoccurrencesPolar Pattern
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.
axisnamefeaturerequiredreversedangle_degworkspaceoccurrencesMirrored
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.
nameplanefeaturerequiredworkspaceLoft
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.
nameruledclosedreversedsketchesrequiredworkspaceSweep
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'.
modenamespinerequiredprofilerequiredworkspacetransitionHelix
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.
nameanglepitchheightradiusworkspaceThickness
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'.
baserequiredjoinmodenamereversedthicknessworkspaceopen_facesrequiredDraft
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.
baserequirednamefacesrequiredreversedangle_degworkspaceneutral_planeList Documents
List all open documents: [{name, label, file_path, dirty, active, object_count}, ...].
workspaceSet Active Document
Switch the active document by name (the value returned from new_document/open_document).
namerequiredworkspaceClose Document
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}.
nameworkspaceTransaction Open
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.
labelworkspaceTransaction Commit
Commit the most recent open transaction; changes are kept.
workspaceTransaction Abort
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.
workspaceAdd Sketch External
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).
refrequiredsketchrequiredworkspaceGet Object
Dump a handle's properties + shape stats. Useful when no dedicated tool exposes what you need.
handlerequiredworkspaceSet Property
Set a single property by name on an object. Coerces lists → Vector for Vector properties; other values pass through.
namerequiredvaluerequiredhandlerequiredworkspaceVerify Feature
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.
handlerequiredtoleranceworkspaceabs_toleranceexpected_delta_mm3requiredSet Visibility
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.
handlerequiredvisiblerequiredworkspaceFillet Edges
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.
edgesrequiredhandlerequiredradiusper_edgeworkspaceallow_partialBoolean Op
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.
oprequiredbaserequiredtoolrequiredstrictworkspaceExport Shape
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.
pathrequiredobjectworkspaceRun Python Script
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}.
coderequiredworkspaceauto_registerRegister Handle
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}.
objectrequiredprefixworkspaceMass Properties
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.
handlerequireddensityworkspaceMake Assembly
Create an App::Part container to hold linked parts. Returns {handle, name}.
nameworkspaceAdd Part
Add a part to an assembly via App::Link. source is one of: {"handle": "<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": "<name>", "parent": "<link-name|handle>", "parent_iface": "<name>"}. Frames come from publish_interface.
matenamesourcerequiredassemblyrequiredplacementworkspaceList Assembly Parts
List parts of an assembly: name, type, linked-target name, position, volume.
assemblyrequiredworkspaceInterference Check
Pairwise interference: compute volume of intersection between every pair of parts. Returns [{a, b, interference_mm3}, ...] descending by volume. Empty list = no interference.
assemblyrequiredworkspaceBOM Extract
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.
densityassemblyrequiredorderablerecursiveworkspacecheck_stockStandard Part Designate
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.
specfamilyhandleworkspacedesignationDesignation Check
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.
rowsassemblyrecursiveworkspaceCatalog Search
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.
kindsizedrivegradelimitfamilylengthstandardworkspacemax_lengthmin_lengthCatalog Nearest
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.
sizerequiredgradelengthstandardrequiredworkspaceCatalog Check
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`.
handleworkspacedesignationEnvelope Check
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.
assemblyrequiredenvelopesrequiredworkspacePublish Interface
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}.
namerequiredframerequiredhandlerequiredworkspaceAnnotate Face
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)}.
facerequiredmetanamerolerequiredhandlerequiredworkspaceList Face Roles
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)
handlerequiredworkspaceDeclare Intent
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.
handlerequiredcontractrequiredworkspaceVerify Intent
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
handlerequiredworkspaceVerify Contract
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:<name> / feature:<name> / intent.
handlerequiredcontractrequiredworkspaceComponent Contract Check
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:<name> (published + sane + in tol), performance_spec:<name> (declared as briefed) and performance:<name> (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:[...], s
briefrequiredhandlerequiredworkspaceInterface Align Check
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.
pairsrequiredtol_mmassemblyrequiredworkspaceValidate Manifest
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.
manifestrequiredworkspaceMerge Assembly
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": {"<id>": {"file": "rel/part.FCStd", "object": "<name>"?, "envelope": {"min":[...],"max":[...]}?}}, "instances": [{"component":"<id>", "name":"<instance>"?, "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.
manifestrequiredworkspaceAssembly Lock
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 <manifest>.lock.json. Returns {lockfile, components}.
lockfilemanifestrequiredworkspaceAssembly Lock Check
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}.
lockfilemanifestrequiredworkspaceMake Drawing Page
Create a TechDraw page using a built-in A4 landscape template by default. template: optional absolute path to a .svg template.
nametemplateworkspaceAdd Projection Group
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'].
bodyrequirednamepagerequiredviewsworkspaceExport Drawing
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}.
pagerequiredpathrequiredworkspaceAdd Dimension
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=<edge tag>: true length of a model edge (the real length, not the foreshortened projection). * view + kind='diameter'|'radius' + edge=<circular edge tag>: ⌀/R of a hole or arc. * view + kind='angle' + face=<conical face tag>: 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.
autoedgefacekindpagerequiredviewviewsto_pointtoleranceworkspacefrom_pointhalf_angleAdd Annotation
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}.
xynamepagerequiredtextrequiredworkspaceAdd Feature Note
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}.
xykindnamepagerequiredtextfeaturerequiredworkspaceSet Title Block
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}.
revdatepagerequiredpartunitsprojectdrawn_bymaterialworkspaceDrawing Gate
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.
pagerequiredprocessworkspacedatums_declaredrequire_balloonedFit Drawing to Page
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).
pagerequiredmarginworkspaceDrawing Legibility
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.
pagerequiredmin_gapworkspaceAdd GD&T Callout
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}.
xynamepagerequiredviewzonerequireddatumscontrolrequiredfeaturemodifiermmc_bonusworkspaceBalloon Drawing
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}.
pagerequiredrenumberworkspaceInspection Plan
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`.
pagerequiredratioworkspaceFAI Report
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.
revpagerequiredpartpathratioresultsreferenceworkspaceAdd Thumbnail
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.
namepagerequiredworkspaceAdd Section View
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.
autonamepagerequiredsymbolprocessworkspaceRender View
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}.
viewedgeswidthhandlerequiredheightworkspacedeflectionRender Views
Render multiple views of a single object. Returns {views: {view_name: {png_base64,...}}}. Default views: ['iso', 'top', 'front'].
viewswidthhandlerequiredheightworkspacedeflectionRender FEM Results
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}.
viewedgesfieldwidthheightanalysisrequiredworkspacedeformation_scaleRender Photoreal
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, s
viewpartsscenewidthdevicehandleheightqualitymaterialrendererworkspaceoutput_dir+1 moreRender Photoreal Submit
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.
viewpartsscenewidthdevicehandleheightqualitymaterialrendererworkspaceoutput_dir+1 moreRender Job
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.
job_idrequireddiscardworkspaceRender Capabilities
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.
workspaceSolve Capabilities
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_<SOLVER>_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`
workspaceSetup Status
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 swi
workspaceverify_imageverify_freecad_bootFEM New Analysis
Create a Fem::FemAnalysis container. Returns {handle, name}.
nameworkspaceFEM Set Solver
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.
kindnameanalysisrequiredtunablesworkspaceFEM Set Material
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.
bodyrequirednameanalysisrequiredmaterialrequiredworkspaceFEM Add Constraint
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.
xyzfluxkindrequirednamerefsrequiredforcex_freey_freez_freeanalysisrequired+9 moreFEM Modal
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.
f_lowf_highn_modesanalysisrequiredworkspaceFEM Modal Results
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}, ...]}.
job_idanalysisworkspaceContact Setup
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}.
nameslopeanalysisrequiredfrictionnonlinearworkspaceface_pairsrequiredFEM Set Nonlinear Material
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}.
nameanalysisrequiredhardeningworkspaceyield_mpayield_pointsbase_materialrequiredramp_incrementsmax_plastic_straintangent_modulus_mpageometric_nonlinearityFEM Buckling
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.
analysisrequiredn_factorsworkspaceFEM Buckling Results
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}, ...]}.
job_idanalysisworkspaceFEM Thermal Results
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: [...]}.
top_njob_idanalysisworkspaceFEM Mesh Refinement
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.
meshrequirednamerefsrequiredworkspacechar_lengthrequiredFEM Mesh
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}.
bodyrequirednameanalysisrequiredworkspacemin_lengthchar_lengthelement_orderFEM Run
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}.
workdiranalysisrequiredworkspaceFEM Run Submit
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.
workdiranalysisrequiredworkspaceFEM Results
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}, ...]}.
top_njob_idanalysisworkspaceFEM Result Probe
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=<body 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?:{...}}.
facefieldpointhandlejob_idanalysisworkspaceFEM Cantilever Demo
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.
forcewidthheightlengthworkdirmesh_sizeworkspaceMaterial Get
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.
namerequiredworkspaceMaterial Select
Ashby-style selection: filter the corpus, then rank survivors. criteria keys are min_<accessor>/max_<accessor> (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.
rank_bycriteriaworkspaceMaterial List
List available materials, optionally filtered to one category ('aluminum' | 'steel' | 'titanium' | 'magnesium' | 'polymer' | 'glass'). Returns {count, category, materials:[{name, category}, ...]} sorted by name.
categoryworkspaceFluid Props
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.
T_KrequiredP_PanamerequiredworkspaceBolted Joint Check
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=K*F*d (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).
k_factormaterialpitch_mmbolt_sizepreload_ntorque_nmworkspacebolt_dia_mmproperty_classexternal_load_npreload_target_pctproof_strength_mpa+1 moreBearing Life
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).
kindspeed_rpmrequiredworkspacedesignationtarget_hoursdynamic_load_c_nequivalent_load_p_nrequiredSpring Check
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.
force_nmaterialworkspacewire_dia_mmrequiredactive_coilsrequireddeflection_mmfree_length_mmcoil_mean_dia_mmrequiredshear_modulus_mpaallowable_shear_mpaGear Rating
Rate spur-gear tooth bending (Lewis): sigma=Ft/(b*m*Y). 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.
teethrequiredpower_wmaterialmodule_mmrequiredworkspaceface_width_mmrequiredpinion_speed_rpmlewis_form_factortangential_force_nallowable_bending_mpaBelt Drive
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}.
power_wrequiredworkspacefriction_coefsmall_pulley_rpmrequiredvbelt_groove_degcenter_distance_mmrequiredtight_side_limit_nlarge_pulley_dia_mmrequiredsmall_pulley_dia_mmrequiredPress Fit Stress
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}.
materialworkspaceshaft_dia_mmrequiredfriction_coefinterference_mmrequiredhub_outer_dia_mmrequiredyoungs_modulus_mpaengagement_length_mmrequiredSeal Check
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}.
workspaceapplicationgroove_depth_mmrequiredgroove_width_mmrequiredmax_gland_fill_pctcross_section_dia_mmrequiredChain Drive
Rate an ANSI roller-chain drive (ASME B29.1). Rated at the lower of the link-plate-fatigue (HP1=0.004*N1^1.08*n1^0.9*P^(3-0.07P), low speed) and roller-impact (HP2=1000*Kr*N1^1.5*P^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.
k_rpower_wstrandsspeed_rpmrequiredworkspaceteeth_smallrequiredchain_numberchain_pitch_mmWeld Group
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=T*r/J from the eccentric moment about the weld centroid, added vectorially at the worst end. required_leg = f_r/(0.707*allowable); 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.
leg_mmforce_nrequiredsegmentsrequiredweld_typeworkspaceload_point_mmrequiredallowable_shear_mpaTolerance Stackup
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.
axisseedchainhandlemethodgeneralsamplesspec_maxspec_minworkspacedefault_tolFit Check
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).
holerequiredshaftrequiredworkspaceFit Class
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).
fitworkspacebasic_sizerequiredGD&T Check
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}.
zonerequiredactualoffsetcontrolrequiredmmc_bonusworkspacedatum_refsFatigue Check
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).
cyclesuts_mpamaterialworkspaceendurance_mpas1000_fractionmean_stress_mpaendurance_cyclesstress_range_mparequiredFracture Check
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.
materialworkspacestress_mparequiredcrack_len_mmrequiredgeometry_factorfracture_toughness_mpa_sqrt_mWear Estimate
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}.
load_nrequiredwear_coefworkspacehardness_mpamax_depth_mmmaterial_pairsliding_dist_mrequiredapparent_area_mm2Creep Risk Flag
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}.
temp_crequiredmaterialworkspacestress_mparequiredmax_service_temp_cHeat Transfer Coefficient Estimate
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}.
prfluidk_w_mknu_m2_sgeometryrequiredworkspacebeta_per_kemissivityt_ambient_ct_surface_crequiredvelocity_m_scharacteristic_mmrequiredAcoustic Screen
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}.
kindrequiredc_m_slx_mmly_mmlz_mmn_modesworkspacet_ambient_cfrequency_hzduct_width_mmneck_area_mm2neck_length_mm+3 morePlate Check
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}.
a_mmb_mmshaperequiredpoissonsupportmaterialworkspaceyoungs_gpadiameter_mmpressure_kparequiredthickness_mmrequiredBeam Buckling
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}.
load_narea_mm2materialwidth_mmheight_mmi_min_mm4length_mmrequiredworkspaceyield_mpayoungs_gpadiameter_mmend_conditionPlastic Collapse
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}.
load_nsupportmaterialwidth_mmrequiredheight_mmrequiredlength_mmrequiredworkspaceyield_mpaElastica Deflection
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}.
i_mm4load_nrequiredmaterialwidth_mmheight_mmlength_mmrequiredworkspaceyoungs_gpaHertz Contact
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}.
load_nrequiredpoisson1poisson2material1material2radius_mmrequiredworkspaceradius2_mmyoungs1_gpayoungs2_gpaLaminate Properties
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, therma
layersrequireddelta_Tforce_nwidth_mmworkspacemoment_nmmMonopole Sphere
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}.
ca_mrequiredr_mrhou_ampfreq_hzrequiredworkspaceRigid Sphere Scattering
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}.
karequireda_mtheta_degworkspaceAcoustic Radiation Submit
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).
cha_mr_mrhomodelu_ampfreq_hzka_listproblemtimeouth_per_wl+4 moreWaveguide Cutoff
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<m><n>'/'TM<m><n>'. 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}.
a_mmrequiredb_mmmodeeps_rfreq_ghzworkspaceDipole Resonance
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}.
freq_ghzlength_mmworkspaceshorteningEM Fullwave Submit
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,
a_mmb_mmnrtseps_rgap_mmn_freqproblemtimeoutlength_mmradius_mmworkspacef_stop_ghz+6 moreFSI Plate Deflection
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}.
i_mm4supportmaterialwidth_mmrequiredlength_mmrequiredworkspaceyoungs_gpapressure_parequiredthickness_mmrequiredFSI Interface Balance
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}.
width_mmrequiredlength_mmrequiredworkspacepressure_parequiredfluid_force_nsolid_reaction_nFSI Channel Pressure
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}.
gap_mmrequiredmu_pa_slength_mmrequiredrho_kg_m3workspacevelocity_m_srequiredFSI Pressure Plate Submit
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.
nu_m2_spoissontimeoutrho_kg_m3workspaceyoungs_paend_time_sdensity_kg_m3time_window_smax_iterationsinlet_velocity_m_sMolding Screen
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}.
materialt_melt_ct_mold_ct_eject_cworkspacealpha_mm2_sflow_length_mmflow_ratio_limitwall_thickness_mmrequiredMoldability Screen
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}.
materialworkspacefail_rationominal_mmwarn_ratioalpha_per_ksink_factort_ambient_ct_solidify_cwall_samplesDrop Impact
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}.
pulsemass_gworkspacedrop_height_mmrequiredcrush_distance_mmdeceleration_limit_gBar Impact
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}.
area_mm2materiallength_mmrequiredworkspaceyield_mpayoungs_gpatangent_mpavelocity_m_srequireddensity_kg_m3Impact Dynamics Submit
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
bodyrequiredgap_mmmethodcontactgravityplasticpoissonsamplesmaterialdirectionworkspaceyield_mpa+11 moreThermal Lumped
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}.
c_ph_convrequiredmass_grequiredpower_wrequiredarea_mm2requiredmaterialworkspaceduration_semissivityt_ambient_cThermal Transient 1D
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}.
kcprhoh_convrequiredmaterialworkspacealpha_m2_sduration_srequiredt_ambient_ct_initial_chalf_thickness_mmrequiredThermal Transient Submit
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 either `k`+`rho`+`cp` (SI) or a `material` name, with optional `t_initial_c` / `t_ambient_c` and mesh/step counts `n_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 BVP `thermal_transient_1d` solves analytically, so the two are directly comparable (the kickoff's relative gate). - **Solve a real FreeCAD solid — the geometry bridge**: pass a `body` handle plus `convection_faces` (1-based indices into the solid's faces; those faces get the `h_conv`/`t_ambient_c` convective BC, every other face is adiabatic), the physics (`h_conv`, `duration_s`, `k`+`rho`+`cp` or `material`), and an optional `char_length_mm` Gmsh element size and `element_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). Prefer `element_order='2nd'` for a sharp transient — quadratic tets resolve the wall gradient accurately even on a coarse mesh. - **Run a prepared `case_dir`** containing 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 c
kcprhosifbodyh_convn_stepscase_dirmaterialworkspaceduration_sn_elements+6 moreThermal Radiation Submit
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 emissivities `emissivity_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_dir`** containing 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).
n_xsift1_ct2_cgap_mk_platewidth_mcase_dirworkspaceemissivity_1emissivity_2plate_thickness_mThermal Composite Wall
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}.
h_inh_outlayersrequiredt_in_crequiredarea_m2t_out_crequiredworkspaceCHT Channel Submit
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}.
nxsift_in_ck_fluidk_solidcase_dircp_fluidlength_mny_fluidny_solidflux_w_m2rho_fluid+4 moreCHT Graetz Submit
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}.
nxnysifgap_mt_in_ck_fluidcase_dircp_fluidlength_mmu_fluidt_wall_crho_fluid+2 moreAcoustic FEM Submit
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}.
klnxnysifkindlx_mly_mc_m_smode_nxmode_nyn_stepscase_dir+4 moreHarmonic Response
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}.
workspacefrequency_hzdamping_ratiorequirednatural_frequency_hzrequiredstatic_deflection_mmHarmonic Response Submit
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}.
nxnysifn_sweeppoissoncase_dirheight_mlength_mspan_pctworkspaceyoungs_patraction_pa+2 more90% of the applicable checks met
How complete this listing is, computed from the registry entry, the endpoint and public sources. Not a rating of the server and not a statement that it is safe or working today: reachability is a dated check, and no installs or reviews go into the score.
Declare the credential headers in the registry entry, or keep the endpoint answering the probe so its auth can be classified.
# 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 PATHpip 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.gitgit clone https://github.com/gchen19/AnkusDrive.git && cd AnkusDrivepython3 -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 reportankusdrive ping # → ping=pong freecad=1.1.1ankusdrive doctor # per-item FreeCAD + solver checklist with the exact fix each# 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 repobash scripts/verify-container-image.sh # the slim image, :latestbash scripts/verify-container-image.sh ghcr.io/gchen19/ankusdrive-solvers@sha256:<digest>tools/build_solver_image.sh --solvers "openfoam fsi" -t my-solvers:dev # 0.69 GB# 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.gitcd AnkusDrivepowershell -ExecutionPolicy Bypass -File scripts\install-core.ps1# Claude Codeclaude 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"] } } }scripts/install-freecad-appimage.sh # or: scripts/install-solvers.sh freecadexport ANKUSDRIVE_FREECADCMD=/path/to/freecadcmd # macOS/Linux$env:ANKUSDRIVE_FREECADCMD = "D:\Apps\FreeCAD\bin\freecadcmd.exe" # Windows# 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.pyFREECAD_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')"{ "mcpServers": { "ankusdrive": { "command": "ankusdrive", "args": ["mcp"] } }}claude mcp add ankusdrive -- ankusdrive mcppip install matplotlib meshio # frame rendering + reading OpenFOAM's VTK outputscripts/install-solvers.sh --optics-gallery ┌────────────┐ ┌────────────┐ ┌──────────────────────┐ │ MCP host │ ───► │ AnkusDrive │ ───► │ freecadcmd worker │ │ (Claude) │ │ (Python) │ IPC │ (long-lived Python) │ └────────────┘ └────────────┘ └──────────────────────┘ ▲ ▲ │ │ │ ▼ └── CLI user ────────┘ .FCStd / .inp / .vtkrun_script(code='''import Pathjob = Path.Job.Create("Job", [_resolve("pad_1")])__result__ = {"job_name": job.Name}''')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'))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')export ANKUSDRIVE_JOURNAL_DIR=~/ankusdrive-journal # or journal_dir = "..." in config.tomlankusdrive journal listankusdrive journal export latest -o transcript.py # --json for the whole recordankusdrive journal export bracket.FCStd -o transcript.py # --json for the whole record