Skip to main content
Glama

Server Configuration

Describes the environment variables required to run the server.

NameRequiredDescriptionDefault
ANKUSDRIVE_CONFIGNoOverride the path to the persistent config file (default: ~/.config/ankusdrive/config.toml or %APPDATA%\ankusdrive\config.toml).
ANKUSDRIVE_ELMER_PATHNoPath to the Elmer solver executable, used by thermal/CHT simulation families.
ANKUSDRIVE_FREECADCMDNoOverride the auto-discovered path to the freecadcmd binary.
ANKUSDRIVE_MAX_WORKSPACESNoMaximum number of named workspaces in the workspace pool.4
ANKUSDRIVE_OPENFOAM_BASHRCNoPath to the OpenFOAM etc/bashrc to source, used by CFD/FSI/injection-molding simulation families.
ANKUSDRIVE_WORKSPACE_IDLE_SNoIdle timeout in seconds before an unused workspace is reaped.900
ANKUSDRIVE_OPTICS_GPL_PYTHONNoPath to a Python interpreter that can import the GPL KrakenOS optics engine for non-sequential tracing.

Instructions

Guidance the server publishes about itself, which clients place ahead of the tool catalog so the model reads it before choosing anything.

This server publishes no instructions, or was last inspected before Glama recorded them.

Capabilities

Features and capabilities supported by this server

Protocol revision2025-11-25

CapabilityDetails
tools
{
  "listChanged": false
}
prompts
{
  "listChanged": false
}
resources
{
  "subscribe": false,
  "listChanged": false
}
experimental
{}

Tools

Functions exposed to the LLM to take actions

NameDescription
pingA

Check that the FreeCAD worker is alive. Returns 'pong' on success.

restart_workerA

Kill the current workspace's FreeCAD worker process and spawn a fresh one. Use when the worker is wedged (e.g. App.ActiveDocument desynced from internal state). All open documents, unsaved changes, and handles in THIS workspace are lost — save first if needed. Other workspaces are untouched. Returns {restarted: True, workspace: , freecad: [...]}.

use_workspaceA

Claim (creating if needed) an isolated workspace and make it the target of subsequent tool calls. Each workspace is its own freecadcmd process with its own ActiveDocument and handle registry — handles do NOT cross workspaces. This is how concurrent agents share one MCP server without clobbering each other's documents: each agent calls use_workspace with a unique name once, up front.

The pool is capped (ANKUSDRIVE_MAX_WORKSPACES, default 4) and idle workspaces are reaped (ANKUSDRIVE_WORKSPACE_IDLE_S, default 900s); claiming a workspace beyond a full pool raises — close an idle one first. Pass "default" to return to the baseline single-agent workspace. Returns {workspace: , freecad: [...], workspaces: [names]}.

list_workspacesA

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. Returns {current, max, idle_reap_s, workspaces: [...]}.

close_workspaceA

Shut down a workspace's worker and free its pool slot. All of that workspace's documents, unsaved changes, and handles are lost. Closing the workspace you are currently in returns you to the "default" workspace. The default workspace can be closed too (its worker respawns clean on next use). Returns {closed: , workspace: , current: }.

versionA

Return FreeCAD and bundled Python versions from the worker.

new_documentB

Create a new FreeCAD document and make it active. Returns {doc: }.

open_documentA

Open an existing .FCStd file, make it active. Returns doc name and object list.

save_documentA

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.

list_objectsA

List objects in the active document. Returns [{name, type, label}, ...].

add_primitiveA

Add a primitive to the active document.

kind: 'box' (uses w, d, h), 'cylinder' (uses r, h), or 'sphere' (uses r). placement: optional [x, y, z] mm translation. name: optional human-facing name. It sets the object's LABEL — what list_objects, bom_extract and the drawing/manifest layers display — and leaves the internal FreeCAD Name alone, since handles and register_handle key off Name and it must stay unique and stable. Omit it and the label stays the type default ('Box' / 'Cylinder' / 'Sphere'), which BOM and designation checks read as an unnamed generic solid. Returns {handle, name, label, volume}: name is FreeCAD's internal id and label is the display name (equal to name when you passed none). The handle (e.g. 'box_1') is how you reference this object in subsequent boolean_op calls.

add_gearA

Add an involute spur gear (FreeCAD's core involute generator), extruded to a solid.

teeth: tooth count (>= 3). module: mm (pitch diameter = module * teeth). height: extrusion thickness mm. pressure_angle: deg (default 20). external: True for an external gear; False for an internal/ring tooth profile. placement: optional [x, y, z] mm translation. Returns {handle, name, volume, pitch_radius, tip_radius, root_radius, teeth, module, external}. Two external gears MESH when their axes are spaced (pitch_radius_a + pitch_radius_b) apart; phase one by half a tooth to avoid tooth-on-tooth interference.

add_rackA

Add a linear gear rack (a spur gear's straight counterpart) as a solid.

A rack is a gear of infinite radius: straight-flanked teeth on a rail. Standard full-depth tooth form (addendum = module, dedendum = 1.25module, tooth height = 2.25module, flanks at pressure_angle from vertical).

teeth: number of teeth (>= 1). module: mm (sets tooth size; circular pitch = module * pi). height: extrusion thickness mm along +Y (the rack's face width; default 6). width: mm, rail base-band thickness below the tooth root line (default 10). pressure_angle: deg, flank angle from vertical (default 20; 0 < pa < 45). placement: optional [x, y, z] mm translation of the rack origin. name: object label (default "Rack").

The profile lies in the XZ plane: root line at z=0, base band from z=-width to z=0, teeth from z=0 to z=2.25*module, extruded along +Y by height.

Returns {handle, name, volume (mm^3), pitch (mm/tooth = modulepi), module, teeth, tooth_height (2.25module mm), length (teethmodulepi mm)}. A spur gear MESHES with this rack when their pitch values match (gear module*pi == rack pitch); length sizes the rail for the travel.

add_sprocketA

Add a roller-chain sprocket (ISO 606 / ANSI), built as a static solid plate.

teeth: tooth count (>= 3). chain_pitch: chain link pitch in mm (e.g. 12.7 for #40 / ANSI 40 chain). roller_diameter: chain roller diameter in mm. height: plate thickness in mm (default 6.0). placement: optional [x, y, z] mm translation.

Build: a disc of tip radius ~= pitch_radius + chain_pitch*0.3 with teeth roller seats (circular pockets, radius roller_diameter/2 * 1.05) cut on the pitch circle, one per tooth. This is a fit/visualisation approximation of the true ISO 606 tooth form, not a load-rated profile.

Returns {handle, name, volume, pitch_diameter, chain_pitch, teeth, tip_radius, bore}. pitch_diameter (mm) = chain_pitch / sin(pi/teeth) and chain_pitch are the MATING numbers: a chain of the same chain_pitch wraps the sprocket, and the centre distance between two sprockets derives from their pitch_diameters. bore is 0 (no shaft hole cut yet — drill one with the hole command).

add_pulleyA

Add a timing-belt (or V) pulley as a static solid. Axis is +Z; toothed belt face spans z in [0, width].

teeth: tooth count (>= 6). belt_pitch: belt tooth pitch mm/tooth (e.g. 2.0 for GT2, 3.0 for GT3/HTD-3M); pitch diameter PD = belt_pitch * teeth / pi. width: belt-face length mm. flanged: True adds two thin guide discs (radius PD/2 + 2*belt_pitch) at each end to retain the belt. height: optional mm; OVERRIDES width when given (default height = width). placement: optional [x, y, z] mm translation of the axis base. name: object label.

Returns {handle, name, volume, pitch_diameter, belt_pitch, teeth, width, flanged}. pitch_diameter (mm) is the mating number: the centre distance to a mating pulley plus the required belt length derive from the two pitch diameters and the same belt_pitch.

add_springA

Add a helical compression spring: a round wire swept along a cylindrical helix.

All lengths in mm; angles n/a. wire_diameter: wire (stock) diameter d, mm. outer_diameter: spring outer diameter OD, mm (must be > wire_diameter). free_length: uncompressed overall length along the axis, mm. coils: number of turns (active coils), may be fractional. kind: 'compression' (only supported mode in v1; end coils are not squared yet). placement: optional [x, y, z] mm translation of the spring's base.

Geometry: mean coil diameter D = outer_diameter - wire_diameter; coil pitch = free_length / coils. Spring rate is computed for STEEL (shear modulus G = 79.3 GPa) as k = Gd^4 / (8D^3*coils), reported in N/mm.

Returns {handle, name, volume (mm^3), mean_diameter (mm), free_length (mm), coils, kind, solid_height (mm, = coils*wire_diameter, the fully-compressed block height), spring_rate_n_per_mm (N/mm)}. Use free_length, solid_height and spring_rate_n_per_mm to spec the spring into a mechanism (available travel = free_length - solid_height; force = spring_rate_n_per_mm * deflection).

add_fastenerA

Add a standard ISO metric fastener (screw / bolt / nut / washer) as a solid.

kind: one of "socket_head_cap_screw", "hex_bolt", "hex_nut", "washer".

  • socket_head_cap_screw: cylindrical head with a cosmetic hex socket + plain shank (threads not modeled).

  • hex_bolt: hex head (across-flats) + plain shank.

  • hex_nut: hex prism with an axial clearance hole.

  • washer: flat annular ring. size: ISO designation, one of "M3","M4","M5","M6","M8","M10","M12". length: shank length in mm. REQUIRED for socket_head_cap_screw and hex_bolt; ignored for nut/washer. grade: optional material / property class AS ORDERED — ISO 898-1 for steel screws ("8.8", "12.9"), ISO 898-2 for nuts ("8", "10"), ISO 3506 for stainless ("A2", "A4-80"). It changes no geometry. What it changes is the ORDERABLE designation stamped on the part: with it you get "ISO 4762 M4×12 A2", which a buyer can quote; without it the designation comes back complete=False saying nobody has chosen between class 8.8 steel and A2 stainless yet. An unrecognised grade is a loud error, never a guess. placement: optional [x, y, z] mm translation of the fastener origin (head top sits at z=0, shank runs in -z for screws/bolts). name: optional object name (default derived from kind).

All dimensions are in mm. Threads are cosmetic (the shank is a plain cylinder of the major diameter).

Returns {handle, name, kind, size, major_diameter, pitch, volume, designation, orderable} plus, by kind: screws/bolts add {length, head_diameter, head_height, model_thread:false}; nut adds {head_diameter (wrench across-flats), head_height}; washer adds {head_diameter (outer diameter), head_height (thickness)}. Mating numbers: drill a through-hole of major_diameter (+ clearance) for the shank; head_diameter sizes a counterbore. designation is the canonical orderable identity ("ISO 4762 M4×12 A2") and orderable its full card. catalog is the off-the-shelf verdict computed at creation time — code stocked, or not_stocked naming the lengths either side. A length nobody stocks is a FINDING, not a refusal: the solid is still built, so you can decide whether to move the stack-up or accept a special.

add_bearingA

Add a deep-groove ball bearing as an assembly envelope solid: an annular ring (outer-diameter cylinder minus bore cylinder) of the given width, axis along +Z. Balls/races are not modeled — this is the fit envelope a coordinator needs to size the shaft, the housing bore, and the shoulder spacing.

Specify dimensions ONE of two ways:

  • designation: a standard metric series code, looked up in a built-in table. Known: "608", "623", "624", "625", "626", "688", "6000", "6200", "6800", "6900". (e.g. "608" -> bore 8, OD 22, width 7 mm.)

  • bore + outer_diameter + width: explicit dims in mm (all three required). Explicit values override a designation's table values when both are given.

bore: inner-bore diameter mm (sizes the shaft). outer_diameter: OD mm (sizes the housing bore). width: axial length mm (shoulder spacing). placement: optional [x, y, z] mm translation of the bearing's near face.

seals: open (default) | RS | 2RS | RZ | 2RZ | Z | 2Z. It changes no geometry — the envelope is identical — but it IS part of the orderable identity: 608, 608-2Z and 608-2RS are three different purchases with different drag, speed limits and prices. It is folded into the canonical designation stamped on the part ("608-2RS").

A bearing built from raw bore/OD/width with NO designation is a dimensional envelope, not a purchasable part, and is deliberately left undesignated rather than given a made-up catalog number.

Raises ValueError if the designation is unknown and dims are incomplete, or if outer_diameter <= bore.

Returns {handle, name, designation, bore, outer_diameter, width, volume, orderable, catalog}. handle starts "bearing_". designation is None when built from explicit dims; orderable is the designation card, whose own designation carries the seal suffix; catalog is the off-the-shelf verdict (stocked or not, None for an undesignated envelope).

oring_grooveA

Compute a static O-ring gland (groove) and optionally cut it into a face.

This is the gland calc designers always fumble, plus an optional cut. Given the O-ring cross-section it returns standard static-seal gland dimensions; with cut=True it also machines the annular groove into a flat face.

cross_section: O-ring wire cross-section diameter in mm (e.g. 1.78, 2.62). Required, > 0. inner_diameter: groove inner diameter in mm (the O-ring's nominal seal ID). Required when cut=True; used to size the returned diameters either way. handle: host solid to cut into (required only when cut=True). face: the flat face to cut the groove into — a stable f_* tag (preferred), a 'FaceN' index string, or an int. Required when cut=True. Must be planar. gland_type: seal-geometry label, default 'static_radial' (informational). compound: optional elastomer + durometer the RING is ordered in ("NBR70", "FKM75", "EPDM70"). Changes no geometry; it completes the AS568 designation of the ring itself — the purchased part this groove exists to hold, which no BOM would otherwise contain because the ring is never a modelled object. cut: True (default) cuts the groove and returns a new solid; False makes this a pure calculator (no geometry, no handle). name: name for the resulting solid when cut=True.

Gland rule (static seal): groove_depth = cross_section0.75 (~25% squeeze, clamped to a 20-30% band), groove_width = cross_section1.30. The groove's inner diameter equals inner_diameter and it spans outward by groove_width.

Returns {groove_depth, groove_width, groove_inner_diameter, groove_outer_diameter, squeeze_pct, cross_section, gland_type, oring} (all mm except squeeze_pct in percent). When cut=True it ALSO returns {handle, name, volume} for the grooved solid; the host input is hidden. mating numbers: cut a groove of inner_diameter to seat an O-ring of that ID; groove_outer_diameter sizes the radial space the groove occupies. oring is the RING's designation card ("AS568-214 NBR70"), or ok=False naming the nearest tabulated sizes when the gland is not an AS568 standard size — an off-table ring is a custom tooled part, and saying so beats naming a dash number that will not seal. oring_catalog is the ring's off-the-shelf verdict.

chamfer_edgesA

Chamfer (bevel) specific edges of a shaped Part object — the direct-shape counterpart to fillet_edges.

handle: handle of the object to chamfer (e.g. 'box_1', a boolean result). edges: non-empty list of edge references. Each may be a tag ('e_...' from list_edges, preferred), an 'EdgeN' string, or a bare 1-based integer index. size: symmetric chamfer leg distance in mm (applied equally to both faces meeting at the edge, i.e. dist1 = dist2 = size). Must be > 0. Default 1.0. name: label for the resulting feature object. Default 'Chamfer'. per_edge: add the edges one at a time, validating after each, instead of in a single apply. Slower; for geometry known to be blend-hostile. allow_partial: accept a partial result instead of aborting. Off by default.

The base object is hidden (consumed into the chamfer feature). Validated exactly like fillet_edges (issue #283) — Shape.isValid(), unchanged solid count, no growth of the tight bounding box — before a handle is issued.

Returns {handle, name, volume (mm^3), edges (resolved 1-based indices actually chamfered), 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 from the document and BlendCheckFailed is raised naming the offending edges — never a handle to corrupt geometry.

shell_solidA

Hollow a raw Part solid into a thin-walled shell (the direct-shape counterpart to thickness, which only works on PartDesign bodies).

handle: handle of the solid to hollow (e.g. a box/cylinder from add_primitive, or any shaped Part::Feature). faces: NON-EMPTY list of the faces to REMOVE — these become the shell's openings. Each entry is a face tag (f_..., from list_faces/query_faces, preferred and edit-stable), a 'FaceN' string, or a 1-based integer index. thickness: wall thickness in mm, must be > 0. The wall is grown INWARD, so the part's outer dimensions are preserved.

The consumed input solid is hidden (its geometry now lives in the shell). Returns {handle (starts 'shell_'), name, volume (mm^3 of the resulting walls), wall_thickness (mm), removed_faces (list of 1-based face indices that were opened)}. Raises if faces is empty, thickness <= 0, an index is out of range, or the offset is too large to produce a valid shell.

add_threadA

Generate a REAL helical ISO-style 60-degree thread as a static solid.

Unlike hole/list_thread_options (which only flag a thread as metadata), this cuts actual helical geometry: a truncated triangular rib swept along a helix and fused to a core cylinder.

All lengths in mm, angles in degrees. diameter: nominal MAJOR (crest) diameter, mm. For internal=True this is the bore the tap fits. pitch: thread pitch, mm per turn (e.g. M8 coarse = 1.25). length: threaded length along +Z from z=0, mm. internal: False (default) -> a finished externally-threaded stud. True -> a TAP/insert cutting-tool solid sized to the bore; fuse it into (or cut it from) a bored hole in your part to produce a threaded bore. starts: number of thread starts, >=1. Multi-start repeats the helix rotated by 360/starts and uses lead = pitch*starts. grade: optional material / property class as ordered ("8.8", "A2"). Changes no geometry; it completes the DIN 976-1 threaded-rod designation stamped on an EXTERNAL single-start thread — that solid is studding, a thing you buy by the metre. An internal thread is a tap-shaped cutting tool and a multi-start is not a stock item, so neither is designated at all. placement: optional [x, y, z] mm translation of the solid's base (default at the origin, axis along +Z). name: optional object name.

Geometry note: the modeled minor (root) uses the ISO 5H/8 truncation; the reported minor_diameter uses the standard ISO formula diameter - 1.0825*pitch. Fallback behaviour: if the helical sweep cannot produce a valid solid the tool returns a plain cylinder tagged with the thread spec and modeled=False (this is rare for sane M-series inputs); always check the modeled flag.

Returns {handle, name, volume (mm^3), major_diameter (mm), minor_diameter (mm), pitch (mm), length (mm), starts (int), internal (bool), modeled (bool), designation, orderable, catalog}. Studding is bought by the bar and cut, so any length up to the longest stock bar reads as stocked with a note that it is a cut. Mating numbers: drill/bore minor_diameter to tap an internal thread; clear a major_diameter (+clearance) hole to pass an external stud.

engrave_textA

Engrave (cut) or emboss (add) extruded text onto a planar face of a solid.

The text is rendered in a system TrueType font, extruded, laid flat on the chosen face centred on its centroid, then booleaned into the host solid.

handle: the host solid to mark. face: the planar face to put the text on — a stable f_* tag (preferred), a 'FaceN' index string, or an int. Must be a flat (planar) face. Get a tag from list_faces / query_faces. text: the string to render (non-empty). size: cap height of the text in mm (default 5.0). depth: extrusion/engraving depth in mm (default 0.5). Engrave recesses the text this far below the surface; emboss raises it this far above. mode: 'engrave' (default) cuts the text into the solid (removes material); 'emboss' fuses raised text onto the surface (adds material). position: optional [u, v] in-face offset in mm from the face centroid, along the text's local X (u) and Y (v) axes. Omit to centre on the face. font: optional absolute path to a .ttf/.ttc font file. If omitted, common macOS fonts are auto-probed (Arial, then Helvetica). If none is found and none is supplied, the call raises RuntimeError — pass an explicit path. name: label for the resulting solid (default 'Text').

Returns {handle, name, volume, text, mode, depth} where volume is the mm^3 of the resulting solid (less than the input for engrave, more for emboss). The host solid is consumed/hidden and replaced by the returned handle.

add_ribA

Add a reinforcing rib/web inside a PartDesign Body by thickening an OPEN sketch profile into a wall that fuses with the body's surrounding material.

Args: body: handle of the PartDesign Body (from make_body) to add the rib to. sketch: handle of a sketch holding an OPEN spine (a single line, arc, or connected polyline) that defines where the rib runs. Must NOT be a closed loop. The sketch's attachment plane sets the rib's orientation. thickness: rib wall thickness in mm (> 0). midplane: if True (default) the wall is centered on the spine, growing thickness/2 to each side; if False it grows from one side. reversed: flip the extrusion sense (use if the rib lands on the wrong side of its sketch plane). name: object label.

Returns a dict: {handle (starts 'rib_'), name, volume (the whole Body's Shape.Volume in mm^3 after the rib — strictly greater than before the rib, since a rib only adds material), thickness}.

Fallback behaviour the caller should know: FreeCAD's native PartDesign::Rib type is unavailable in AnkusDrive's headless runtime, so the rib is built as an equivalent midplane PartDesign::Pad — the open spine is offset by +/-thickness/2 into a closed footprint and padded across the body so it reaches the surrounding walls. For the usual straight or smoothly-curved spine this matches a Rib; very intricate spines may differ from the native tool. Raises ValueError if the profile is closed/empty/degenerate or thickness <= 0, and RuntimeError if the rib adds no material (spine does not span between walls).

transformA

Move and/or rotate an existing object in place — first-class replacement for hand-poking an object's Placement via set_property.

handle: object to move (any object with a Placement: primitive, body, feature). translate: [x, y, z] translation in mm (default no translation). rotate_axis: rotation axis as a 3-vector [x, y, z] (need not be unit length; default [0, 0, 1], the Z axis). angle: rotation about rotate_axis in DEGREES (default 0 = no rotation). relative: True (default) composes this move ONTO the object's current placement (incremental); False sets it as the ABSOLUTE placement, discarding the object's prior placement.

The same object is moved — NO new handle is created. The rotation is applied about the object's local origin (combine with translate to pivot elsewhere).

Returns {handle, name, placement: {base:[x,y,z] mm, axis:[x,y,z], angle_deg}} describing the object's resulting placement.

scale_shapeA

Scale a shape uniformly or per-axis, baking a fresh static solid.

Scaling breaks parametric history, so this produces a standalone Part::Feature (not a linked/parametric feature); the source object is hidden since its geometry is consumed into the scaled copy.

handle: source shape handle. factor: scalar for uniform scale, or [sx, sy, sz] for per-axis scale. All factors must be > 0. center: optional [x, y, z] mm pivot to scale about; when omitted the scale is about the world origin (so the shape also moves away from/toward origin). name: object label (default 'Scaled').

Lengths in mm. Returns {handle, name, volume, factor} where factor is the normalized [sx, sy, sz] applied and volume (mm^3) equals the source volume times sxsysz.

copy_shapeA

Duplicate a shaped object as an INDEPENDENT static solid.

Unlike add_part (which creates an App::Link that tracks the source), this deep-copies the geometry: later edits to the original do NOT propagate to the copy. Use it to seed a mirror/pattern, or to drop a standalone duplicate instance into an assembly.

handle: handle of the source object (must have a Shape). placement: optional absolute [x, y, z] translation in mm applied to the copy's base. Omit to leave the copy coincident with the source. The source object is unchanged and stays visible. name: optional name for the new object (default '_copy').

Returns {handle, name, volume}: handle is a new 'copy_N' handle, name is the FreeCAD object name, volume is the copied solid's volume in mm^3.

measure_distanceA

Minimum distance between two entities, in mm. The workhorse measurement tool: lets a blind agent verify gaps, clearances, and contact.

Args: a: handle of the first object. b: handle of the second object. a_ref: optional sub-shape selector on a to measure FROM instead of the whole solid -- an f_* face tag, an e_* edge tag, or a literal "FaceN"/"EdgeN" (1-based). Omit to use the whole shape. b_ref: optional sub-shape selector on b (same forms as a_ref).

Measures the minimum (closest-approach) distance, so distance_mm = 0 means the two entities touch or interpenetrate. This does NOT report overlap volume -- use min_clearance / interference_check for penetration depth.

Returns a dict (no handle; this is a measurement): distance_mm: float -- minimum gap in mm (0.0 when touching/intersecting). point_on_a: [x, y, z] mm -- closest point on a (or its sub-shape). point_on_b: [x, y, z] mm -- closest point on b (or its sub-shape). touching: bool -- True when distance_mm < 1e-7.

measure_angleA

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.

bounding_boxA

Axis-aligned bounding box (AABB) of a shaped object. All lengths in mm, in world coordinates. This is a measurement — it returns numbers, not a new object, and does not modify the model.

KNOWN QUIRK (issue #284): min/max/size are FreeCAD/OCC's ANALYTIC box, which is an UPPER bound, not the true extent. OCC boxes a trimmed face using its untrimmed carrier surface, so a planar cut through fillets, chamfers, lofts or a sphere can report several mm of material that is not there — a real case had a trim plane at X=-32.0 reported as X=-36.7. Before you conclude a part is the wrong size, check "verified" (and pass tight=True): the analytic box over-estimating a correct part looks exactly like a wrong part.

handle: the object to measure. oriented: if True, also compute the tightest box at any orientation (the oriented bounding box, OBB) and return it under "oriented"; if the build can't compute it, "oriented" is null. Default False. This is about ORIENTATION, not tightness — it comes from the same analytic geometry and inherits the same over-estimate. tight: if True, also tessellate the shape and return the mesh-derived box under "tight" — the trustworthy numbers when the analytic box over-estimates. Opt-in because tessellation is not free (~1.4s on a 200mm plate with 60 filleted holes). Default False. deflection: mesh chord tolerance in mm for tight=True. Default diagonal/2000 (floor 0.001mm); larger is coarser and faster.

Returns a dict: min [x,y,z] mm — lower corner of the analytic AABB (upper bound) max [x,y,z] mm — upper corner of the analytic AABB (upper bound) size [x,y,z] mm — extents (max - min) along X, Y, Z center [x,y,z] mm — AABB center point diagonal float mm — space-diagonal length of the AABB oriented null, or {size:[x,y,z] mm, center:[x,y,z] mm, diagonal: mm} when oriented=True and supported — the minimum-volume box at the shape's best orientation (size is its three edge lengths). verified how far min/max above can be trusted: "exact" — proven tight (the shape's own vertices reach all six faces of the analytic box). "mesh_agrees" — tight=True found no disagreement beyond the mesh tolerance. "unverified" — unproven, the usual verdict on a curved part. Treat min/max/size as an upper bound only, and re-run with tight=True to measure. "over_estimate" — tight=True proved the analytic box overshoots. Use "tight"; min/max/size are wrong-big. tight null unless tight=True, else {min, max, size, center, diagonal, deflection, triangles} measured off the mesh. Accurate to about deflection; the true box lies between "tight" and the analytic box, never outside them. warnings list of strings (empty when there is nothing to say): which face over-estimates and by how many mm, or that an unverified box has not been checked.

min_clearanceA

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").

check_shapeA

Check a shaped object's geometry validity and topology before you build on it. Inspection only — measures, returns no handle, mutates nothing, and does NOT auto-repair. Use it as a guard after booleans/sweeps/imports to confirm you have one clean watertight solid.

Note: a watertight solid can still have a BLOCKED or LEAKY enclosed-flow path — watertightness says the shell is closed, not that an internal channel is unobstructed and leak-free. For ducts/manifolds/adapters use check_airtight_path(inlet, outlet) to verify the flow path.

handle: the object to inspect.

Returns a dict (volumes in mm3): valid (bool) OCC topology/geometry is sound watertight_solid (bool) exactly one solid AND valid AND closed — the 'safe to keep building' verdict shape_type (str) e.g. 'Solid', 'Shell', 'Compound', 'Wire' closed (bool) no free boundary edges solids (int) number of solids (want 1 for a part) shells (int) number of shells faces (int) number of faces edges (int) number of edges volume_mm3 (float) total volume (0 for open/2D shapes) is_null (bool) the shape is empty check (str) present only when valid is False — diagnostics were printed to the worker log check_error (str) present only if the diagnostic pass itself raised

check_airtight_pathA

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

classify_face_sidesA

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

section_viewA

Cut a solid with a plane and return the cross-section it exposes. This is the best way to "see inside" a part blind: it measures the cut area and its extent, and can optionally emit the section outline as a new object for rendering/export. Units: mm (lengths), mm^2 (areas).

handle: the solid to slice (a AnkusDrive handle). plane: "XY", "XZ", or "YZ" (world datum planes) OR a datum-plane handle. World normals follow FreeCAD: XY -> +Z, XZ -> -Y, YZ -> +X. A datum handle uses its local +Z as the cutting normal. offset: shift of the cutting plane along its normal, in mm (default 0 = the plane through the world origin / datum origin). E.g. plane="XY", offset=10 cuts at z=10. emit_profile: when True, add a Part::Feature holding the section wires to the document, register it, and return its handle (raises if the plane misses the shape). Default False = measure only, no new geometry. name: object name for the emitted profile (only used when emit_profile=True).

Does not modify the input geometry. Returns a dict: plane: str (echoed), offset_mm: float (echoed), normal: [x, y, z] unit cutting-plane normal, section_area_mm2: float — total area of the closed cross-section wires, wire_count: int — number of section wires found (0 means the plane misses the shape), closed_wire_count: int — how many of those wires are closed, bbox: {min:[x,y,z], max:[x,y,z], size:[dx,dy,dz]} of the section, or None when the plane misses the shape, handle: str — handle of the emitted profile (ONLY when emit_profile=True), name: str — its FreeCAD object name (ONLY when emit_profile=True).

list_facesB

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.

list_edgesA

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.

query_facesB

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.

resolve_faceA

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).

resolve_edgeC

Resolve an edge tag to the current EdgeN index. Errors on miss or ambiguity.

make_bodyB

Create a PartDesign Body. Subsequent sketches/features go inside it.

Returns {handle, name}.

make_datum_planeB

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.

make_sketchB

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}.

add_sketch_geometryA

Append geometric primitives to a sketch.

items is a list of dicts:

  • {type: 'line', start: [x,y], end: [x,y], construction?: bool}

  • {type: 'circle', center: [x,y], radius: float}

  • {type: 'arc', center: [x,y], radius, start_angle, end_angle} (radians)

  • {type: 'point', pos: [x,y]} Returns {indices: [...]} — Sketcher-assigned indices for use in constraints.

add_sketch_constraintB

Add a constraint to a sketch.

type: Coincident, Horizontal, Vertical, Distance, DistanceX, DistanceY, Radius, Diameter, Equal, Parallel, Perpendicular, Tangent, Block, Symmetric, Angle. refs: list of [geom_idx, vertex_role] pairs. vertex_role: 0=edge, 1=start, 2=end, 3=center. value: numeric value (mm or radians) for dimensional constraints.

close_sketchD

Recompute and report DOF status. Returns {geometry_count, constraint_count, open_vertices, fully_constrained}.

padC

Pad a sketch by length mm. symmetric=True extrudes both directions.

pocketA

Subtract a pad of length mm from the body. through_all ignores length.

through ('wall'|'body'): preferred over through_all. 'wall' ray-casts the body to find the first exit boundary and cuts exactly one wall thick — correct for solids (one wall = full thickness) AND shelled bodies. 'body' is the legacy ThroughAll; on a shelled body it punches through every wall and ruins the cavity. Implies direction='into_body'. Result carries wall_depth_mm so the caller can verify. direction (preferred over reversed): 'into_body' makes the cut actually remove material; 'away_from_body' extrudes outside the body. The tool probes both Reversed values and picks the one matching intent. reversed: legacy raw flag, used only if neither through nor direction is set.

revolveC

Revolve a sketch around a body origin axis ('X'|'Y'|'Z') by angle deg.

partdesign_filletA

PartDesign Fillet on edges of a feature in a Body. edges accepts e_* tags or 'EdgeN' index strings; radius in mm (> 0).

Validated before a handle is issued, like fillet_edges (issue #283): Shape.isValid(), unchanged solid count, and no growth of the tight bounding box. PartDesign needs it as badly as the Part workbench — r=0.6 on a 40x40x1 pad returns an 'Up-to-date' single solid 15% LARGER than the pad.

per_edge: add the edges one at a time, validating after each. allow_partial: accept a partial result instead of aborting. Off by default.

Returns {handle, name, volume (mm^3), edges (the 'EdgeN' names actually filleted), checks {valid, solids, envelope_ok, envelope_growth_mm, envelope_tol_mm}, mode ('batch' | 'per_edge'), partial}; when partial is True, also skipped_edges and a warnings entry. On a failed check the feature is removed, the Body's Tip is restored, and BlendCheckFailed is raised naming the offending edges and the subset that does fillet cleanly.

partdesign_chamferA

PartDesign Chamfer on edges of a feature in a Body. edges accepts e_* tags or 'EdgeN' index strings; size is the chamfer leg in mm (> 0).

Validated and rolled back on failure exactly like partdesign_fillet (issue #283), with the same per_edge / allow_partial opt-ins.

Returns {handle, name, volume (mm^3), edges (the 'EdgeN' names actually chamfered), checks {valid, solids, envelope_ok, envelope_growth_mm, envelope_tol_mm}, mode, partial}; when partial is True, also skipped_edges and a warnings entry. On a failed check the feature is removed, the Body's Tip is restored, and BlendCheckFailed is raised naming the offending edges.

holeA

Drill a parametric Hole from a sketch (one or more circles).

sketch: handle of a sketch placed on a face of an existing body feature. depth_type: 'Dimension' (use depth) or 'ThroughAll'. cut_type: 'None' | 'Counterbore' | 'Countersink' | 'Counterdrill'. When non-None, cut_diameter (head clearance) and cut_depth apply. threaded=True applies a tap. thread_type / thread_size are COUPLED enums — valid thread_size values DEPEND on thread_type ('M4' fits 'ISOMetricProfile' but not 'UNC'). Use list_thread_options() to discover thread_type values and list_thread_options(thread_type=...) for that type's valid sizes. intended_for ('print'|'machine'|'drawing'): drives ModelThread default when threaded=True so the caller doesn't have to know what ModelThread means. print → ModelThread=True. Required for 3D-printed threaded holes — the screw must engage the printed thread geometry; a smooth pilot won't tap itself. machine → ModelThread=False. CAM software reads thread metadata and drives a physical tap. Modeling thread bloats files and fights patterns/fillets. drawing → ModelThread=False. Drawings annotate threads symbolically. Explicit model_thread overrides intended_for. through ('wall'|'body'): preferred over depth_type/depth. 'wall' ray-casts to the first exit boundary and drills exactly one wall thick — critical on shelled bodies where 'body' (ThroughAll) would destroy the cavity. Implies direction='into_body'. Result carries wall_depth_mm. direction (preferred over reversed): 'into_body' picks the Reversed value that actually removes material; 'away_from_body' picks the value that removes none. Hole and Pocket interpret the raw flag differently. reversed: legacy raw flag, used only if neither through nor direction is set.

list_thread_optionsA

Discover the COUPLED ThreadType / ThreadSize enums on the hole tool.

Call with no args to list valid thread_type values. Call with thread_type=... to list the valid thread_size values for that type (the coupling: thread_size='M4' is valid for 'ISOMetricProfile' but not for 'UNC'). Use this BEFORE calling hole(threaded=True, thread_type=..., thread_size=...) to avoid a failed enum-value call.

Returns either {thread_types: [...]} or {thread_type, thread_sizes: [...]}.

linear_patternC

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.

polar_patternC

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.

mirroredB

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.

loftB

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.

sweepA

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'.

helixA

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.

thicknessA

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'.

draftB

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.

list_documentsA

List all open documents: [{name, label, file_path, dirty, active, object_count}, ...].

set_active_documentA

Switch the active document by name (the value returned from new_document/open_document).

close_documentA

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}.

transaction_openA

Begin an undoable transaction on the active document. Pair with transaction_commit (keep the changes) or transaction_abort (roll back). Transactions nest — the most-recent open is committed/aborted first.

transaction_commitA

Commit the most recent open transaction; changes are kept.

transaction_abortA

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.

add_sketch_externalA

Project an external edge/face/vertex into a sketch as construction geometry.

ref: {handle, edge: tag|'EdgeN'} (or face/vertex variant; or {handle, tag} where tag is e_/f_ prefixed). The projected element gets a negative geom index so subsequent constraints can reference it. Use this to make a sketch that stays anchored to upstream geometry (e.g. a hole 5mm from a tagged edge that survives pad-length edits).

get_objectC

Dump a handle's properties + shape stats. Useful when no dedicated tool exposes what you need.

set_propertyB

Set a single property by name on an object. Coerces lists → Vector for Vector properties; other values pass through.

verify_featureA

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.

set_visibilityA

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.

fillet_edgesA

Fillet edges of a shaped Part object. edges accepts tags (e_... from list_edges, preferred), 'EdgeN' strings, or bare 1-based ints. radius is the blend radius in mm (> 0).

Every result is validated before a handle comes back (issue #283): this OCC build's fillet is edge- and order-sensitive enough to produce corrupt geometry with no exception at all — a 20 mm cube filleted on all 12 edges at r=11 returns one solid with a LARGER volume and a 13 mm larger bounding box. The checks are Shape.isValid(), an unchanged solid count, and no growth of the tight bounding box (a fillet only removes or holds the envelope).

per_edge: skip the single-shot apply and add the edges one at a time, validating after each. Slower (one recompute per edge); for geometry already known to be blend-hostile. allow_partial: accept a partial result instead of aborting. Off by default.

Returns {handle, name, volume (mm^3), edges (the 1-based indices actually filleted), checks {valid, solids, envelope_ok, envelope_growth_mm, envelope_tol_mm}, mode ('batch' | 'per_edge'), partial}. When partial is True the reply also carries skipped_edges and a warnings entry saying the solid is NOT the part that was asked for.

On failure the handler retries per edge to find the culprits, removes the failed feature from the document and raises BlendCheckFailed naming the offending edges and the subset that does fillet cleanly. It never returns a handle to corrupt geometry, and never quietly drops a fillet unless allow_partial was asked for.

boolean_opA

Boolean operation on two existing objects, referenced by their handles.

op: 'cut' (base minus tool), 'fuse' (union), or 'common' (intersection). base, tool: handles returned from add_primitive (e.g. 'box_1', 'cylinder_1'). strict: raise instead of warning on a degenerate cut (see below).

Returns {handle, volume, removed_volume, volume_ratio}, plus warnings — a list of strings — ONLY when the cut looks degenerate; the key is absent on a clean op, so "warnings" in result is the test. removed_volume: base_volume - result_volume, mm3. Positive means material went away (always so for cut/common); NEGATIVE on a fuse, where it is the volume the tool added. volume_ratio: result_volume / base_volume, or None when the base was empty. The two warned cases are cut-only, and each means the cut did not do what was asked: annihilation (result ~ 0 — the tool swallowed the base, so every later feature operates on nothing) and miss (result == base — the tool never intersected the base, so nothing was removed). Warn-don't-fail is the default because cutting everything away is legitimate in some workflows; pass strict=True in a scripted recipe to turn both into an error instead.

export_shapeB

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 first shaped object in the active document.

run_scriptA

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.

Returns {result, registered}.

register_handleA

Register an existing FreeCAD object into the AnkusDrive handle table. Use after run_script (when auto_register=False) or after open_document to bring objects into the handle ecosystem so subsequent tool calls accept them via handle.

object: the FreeCAD object's .Name (e.g. 'Helix001', 'Cut'). prefix: handle prefix (default 'manual'). Each call returns a fresh handle; registering the same object twice produces two aliases. Returns {handle, name, type, label}.

mass_propertiesA

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.

make_assemblyB

Create an App::Part container to hold linked parts. Returns {handle, name}.

add_partA

Add a part to an assembly via App::Link.

source is one of: {"handle": ""} — link an in-doc body {"path": "/path/to/part.FCStd"} — link first body / subassembly {"path": "/path/to/part.FCStd", "object": "X"} — link named object placement: [x, y, z] or {position: [...], axis: [...], angle_deg: ...}. mate: place by aligning this part's published interface frame to an already-placed parent's, instead of (or after) a raw placement: {"child_iface": "", "parent": "<link-name|handle>", "parent_iface": ""}. Frames come from publish_interface.

list_assembly_partsB

List parts of an assembly: name, type, linked-target name, position, volume.

interference_checkA

Pairwise interference: compute volume of intersection between every pair of parts. Returns [{a, b, interference_mm3}, ...] descending by volume. Empty list = no interference.

bom_extractA

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.

standard_part_designateA

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.

designation_checkA

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.

catalog_searchA

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.

catalog_nearestA

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.

catalog_checkA

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.

envelope_checkA

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.

publish_interfaceA

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}.

annotate_faceA

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)}.

list_face_rolesA

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)

declare_intentA

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.

verify_intentA

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

verify_contractA

Build-time self-check of a component against its manifest slice (RFC §11.3): a builder calls this on its OWN part before save, so a contract violation is caught locally and cheaply instead of after a fan-in merge (build → merge → gate-fail → rebuild becomes build → self-check → fix). Never raises on a failing check (a failure is a passed=False row), so it is safe to call in a loop. Inspection only; mutates nothing.

handle: the component's shaped object. contract: the component's slice — all keys optional, give at least one: envelope {min:[x,y,z], max:[x,y,z]} the part's LOCAL bbox must fit in it. interfaces {name: {origin:[x,y,z], z_axis?:[x,y,z], tol_mm?, angle_tol_deg?}} each named frame must be PUBLISHED (publish_interface) and within tolerance of the contracted origin (and axis, if z_axis given) — catches "forgot to publish" / "published in the wrong place". features [ {kind, ...} ] per-feature self-checks: {kind:"gear", module_mm, teeth, internal?, tol_mm?} {kind:"bore", diameter_mm, tol_mm?} {kind:"extent", axis:"x"|"y"|"z", length_mm, tol_mm?} intent bool also run verify_intent.

Returns {handle, ok, results:[{check, passed, detail}]} — ok True iff every check passed; check names are envelope / interface: / feature: / intent.

component_contract_checkA

Builder-side contract gate for one component (issue #169) — the local half of the gate merge_assembly re-runs at fan-in. Any MCP host builds a component with the full AnkusDrive tool surface, then calls this on its part BEFORE saving, against its builder brief (a ankusdrive.builder_brief/1 slice), and repairs any failing check. Catching a violation here turns the expensive loop (build → merge → gate-fail → rebuild) into a cheap local one. Never raises on a failing check.

handle: the component's shaped object. brief: a builder brief. Only three of its keys drive checks (the rest guide the build, not the gate): envelope {min:[x,y,z], max:[x,y,z]} the part's LOCAL bbox must fit inside. interfaces {name: {origin?:[x,y,z], z_axis?:[x,y,z], tol_mm?, angle_tol_deg?}} each named frame must be PUBLISHED (publish_interface) with a sane frame, and within tolerance of a pinned origin/axis if the brief gives one. performance {requirements?: [{name, limit}], required?: bool} the quantitative spec (#226) the builder must DECLARE (declare_performance, no looser than briefed) and PROVE (verify_performance) before fan-in.

Checks run: watertight (check_shape's one-clean-solid verdict), envelope (local bbox inside the keep-out box), interface: (published + sane + in tol), performance_spec: (declared as briefed) and performance: (the last RECORDED verify_performance verdict says it is met).

A performance requirement the record says is NOT met fails the gate. One with no verdict yet — never verified, a solve still in flight, or a verdict invalidated by a later edit — is neither passed nor failed: it comes back in skipped with a reason, because "unverified" is not "fine" and must not be actioned as either.

Returns {handle, ok, checks:[{check, passed, detail}], reasons:[...], skipped:[{check, reason}], performance?} — ok True iff every check passed (skips never move it); reasons is the failing checks' details. A part that declares no performance contract gets no performance rows, empty skipped and no performance key, so the geometric gate is unchanged.

interface_align_checkA

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.

validate_manifestA

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.

merge_assemblyA

Construct-up an assembly from a manifest JSON (the coordinator's one call): create the doc, link each component by file path, place it, recompute, and run the gates. Component files resolve relative to the manifest's directory; links auto-reload, so re-running picks up updated components (deterministic, idempotent).

manifest shape: { "name": "gearbox", "root": "gearbox.FCStd", "components": {"": {"file": "rel/part.FCStd", "object": ""?, "envelope": {"min":[...],"max":[...]}?}}, "instances": [{"component":"", "name":""?, "placement": [x,y,z] | {position,axis,angle_deg}, "mate": {"child_iface","parent","parent_iface", "verify_align":{"child_iface","parent_iface"}?}?}], "mates": [{"child","parent","child_iface","parent_iface", "verify_align":{...}?}]? }

Placement positions anchors; mate-by-frame positions everything else by aligning published interface frames (see publish_interface).

Any component carrying a PERFORMANCE contract (declare_performance, #226) is gated on it too, with no manifest opt-in: the merge consults the verdict verify_performance last RECORDED on that part and never measures, so it stays synchronous and deterministic. A requirement measured as NOT met fails the merge; a requirement with no verdict yet is neither passed nor failed and rides in report["performance"]["skipped"] — "unverified" is never read as "fine".

Returns {assembly, doc, root, placed, gates:{interference, bom, envelope, interface_align?, typed?, requirements?, mobility?, performance?}, ok, requirements?, mobility?, performance?, children?, library?}. The performance gate and report block are absent entirely when no component declares a contract.

assembly_lockA

Write a lockfile recording each component's content hash, published- interface hash, and mate dependencies — the provenance baseline a coordinator uses to detect drift across a team. Call after a clean merge. lockfile defaults to .lock.json. Returns {lockfile, components}.

assembly_lock_checkA

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}.

make_drawing_pageC

Create a TechDraw page using a built-in A4 landscape template by default. template: optional absolute path to a .svg template.

add_projection_groupB

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'].

export_drawingB

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}.

add_dimensionA

Add dimension(s) to a drawing page.

Modes (pick one):

  • auto=True: overall horizontal + vertical extent dimensions for every part-view (or only those named in views, by name or projection code).

  • view + edge=: dimension the true length of a model edge, projected into that view. The printed value is the real measured length, not the foreshortened projection.

  • view + kind='diameter'|'radius' + edge=: a ⌀/R dimension of a hole or arc.

  • view + kind='angle' + face=: the half-angle (or, by default, the 2× included angle) of a conical face — the curved angle a machinist sets for a chamfer cone / countersink / taper (issue #108). Pass half_angle=True to call out the half-angle instead.

  • view + from_point/to_point ([x,y,z] model points): dimension between two 3D points. view: a view handle, object name, or projection code ('Front', 'Top', ...). kind: 'aligned' (default) | 'horizontal' | 'vertical' | 'diameter' | 'radius' | 'angle'. tolerance: optional, rendered next to the value (a machinist needs it to make the part to size): {"sym": 0.1} for ±0.1, {"plus": .., "minus": ..} for an asymmetric tolerance, or {"fit": "H7"} / {"fit": "H7/g6"} to look up ISO 286 hole-side limits at the dimension's basic size. Returns {dimensions: [{handle, name, type, value}, ...]} — value is the true measured size of each dimension created.

add_annotationA

Add a free text annotation to a drawing page at page position (x, y) in mm (origin bottom-left, +Y up, matching TechDraw view placement). Returns {handle, name, text}.

add_feature_noteA

Attach an explicit manufacturing NOTE that satisfies a curved/periodic feature the drawing_gate enumerates (issue #108) — the 'per CAD model / profile table' coverage for geometry a single number can't capture (a freeform/BSpline wall's profile, a tooth pattern's full parameter set) or a documented cone angle. feature is the enumerated feature id (e.g. 'FREEFORM1', 'PAT1', 'CONE1', from drawing_gate's enumerated_features); text defaults to a sensible callout. The gate reads the note back as coverage. Returns {handle, name, feature, text}.

set_title_blockA

Populate the drawing's title block. FreeCAD's default template is a bare sheet, so AnkusDrive composes its own block in the bottom-right corner on SVG/PDF export. Scale, sheet size, units, and part name are auto-derived from the page; the fields here override or add to them (a machinist needs material + scale + units to cut from the sheet). Calling this opts the page into rendering the block. Returns {handle, name, fields}.

drawing_gateA

Manufacturing-completeness gate for a drawing page: does the placed dimension set fully and non-redundantly reconstruct the part? A green render is not a manufacturable drawing — this validates the drawing itself, the way the geometry-realizes-declaration gate validates an assembly.

Reads the real solid + the placed dimensions and accounts degrees of freedom, process-aware: a 'prismatic' (milled/plate) part must locate each hole by X/Y from a datum and size the block W×H×T; a 'turned' part is concentric, so a step needs only Ø + axial length. process='auto' infers it from the geometry.

Returns {ok, violations, slots_total, slots_covered, process, features, dimensions, enumerated_features, datum_faces, section_recommended}. section_recommended ({recommended, reasons, feature_ids}) advises whether the part has internal geometry that needs a cross-section (see add_section_view). Each violation has a code (under = a feature size/location is missing; redundant = a DOF dimensioned more than once; conflict = dimensioned twice with disagreeing values; extra = a dim that pins nothing; no_datum = a location not taken from a datum) and a human reason. ok=True (empty violations) means the drawing is manufacturing-complete.

Datum-origin discipline turns on automatically when the part has faces annotated role='datum' (annotate_face): a location dimension not measured from a datum face is then flagged no_datum. Set datums_declared=True to force the check on even without annotated datums.

require_ballooned=True additionally demands that every characteristic carries an inspection balloon (see balloon_drawing) — the requirement a release flow imposes when the drawing must ship with an inspection plan. Unballooned characteristics become not_ballooned violations and fail the gate. The ballooned ({ok, total, ballooned, missing}) summary is reported either way.

fit_pageA

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).

drawing_legibilityA

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.

add_gdt_calloutA

Place a GD&T feature control frame on a drawing page.

Declaring geometric tolerance ON THE DRAWING (rather than only checking a measurement with gdt_check) is what makes it inspectable: the frame renders as a real compartmented symbol, and inspection_plan / fai_report read it back as a characteristic with its own balloon and measurement method.

control: an ASME Y14.5 geometric characteristic — the same vocabulary gdt_check accepts: flatness, straightness, circularity, cylindricity, profile_line, profile_surface, perpendicularity, parallelism, angularity, position, concentricity, runout, total_runout. zone: tolerance zone in mm (rendered with a Ø for the diametral controls — position, concentricity, circularity, cylindricity). datums: the ordered datum reference frame, e.g. ["A", "B", "C"]. A control with datums is CMM work; a datum-free form control is surface-plate work, and the inspection plan picks the instrument accordingly. feature: optionally the enumerated feature id (drawing_gate's enumerated_features) the frame controls. mmc_bonus: material-condition bonus tolerance carried into inspection, mm. modifier: free text printed in the tolerance compartment (e.g. "Ⓜ"). x / y: page position in mm (origin bottom-left, +Y up, like add_annotation).

Returns {handle, name, control, zone, datums, text}.

balloon_drawingA

Number every characteristic on a drawing page with an inspection balloon — each dimension, feature control frame, and feature note gets a numbered circle beside it, rendered on SVG/PDF export. This is the print a quality engineer actually works from, and the key inspection_plan and fai_report row against.

Balloon numbers are an IDENTITY, not an ordinal. They are persisted on the FreeCAD objects, so: re-running on an unchanged page reassigns nothing; adding a dimension appends the next number rather than renumbering the print; and a deleted dimension RETIRES its number instead of passing it to a different feature — an inspection record written against balloon 7 can never come to mean something else. Pass renumber=True to deliberately discard the numbering and start from 1 (which invalidates any inspection record already written).

Call it after the dimensions are placed and fit_page has run. Returns {count, balloons, assigned, kept, retired, next_balloon}; balloons maps each source object name to its number.

inspection_planA

The characteristic list for a drawing page as data: every dimension, feature control frame, and feature note, ballooned, with nominal, limits, and a suggested measurement method per row.

The method follows the tolerance rather than a guess: the gauge-maker's ratio:1 rule (default 10:1 — the instrument must resolve a tenth of the tolerance band) walked down a per-family instrument ladder, so a loose feature isn't sent to the CMM and a tight bore isn't signed off with a caliper. A bore takes the pin/bore gauge ladder (a micrometer can't reach inside one); a GD&T control referencing a datum frame is CMM work; a datum-free form control is surface-plate work. The required resolution is exact arithmetic, the 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 a characteristic cannot be inspected as drawn — an untoleranced size the inspector has no limits to accept or reject against (code no_tolerance), or a band finer than any instrument on its ladder (code no_instrument) — with unmeasurable naming which and why.

fai_reportA

First-article inspection report for a drawing page, shaped like AS9102 Rev B Form 3.

Each ballooned characteristic becomes a row carrying the AS9102 fields (Char No. / Reference Location / Characteristic Designator / Requirement / Results / Designed-Qualified Tooling / Nonconformance Number / Notes) plus its limits, the suggested measurement method, and a computed status.

results: balloon number -> measured value; each row is then accepted or rejected against its limits. For a position control you may pass {"x":.., "y":..} and the diametral deviation 2·√(x²+y²) is used, matching gdt_check. Omit it entirely to get a BLANK form for the inspector — every row comes back 'not_evaluated', never a silent pass. path: optionally write the report — .csv (the data), .svg or .pdf (a printable paginated table). part / rev: identity stamped into the file; default to the page's part name and title-block revision. reference: AS9102 field 6 (Reference Location), e.g. the sheet/zone; defaults to the view each characteristic is dimensioned on.

THIS IS NOT A CERTIFIED AS9102 SUBMISSION — it reproduces the Form 3 field layout so a real form can be filled from it, and says so on every artifact it writes.

Returns {ok, columns, rows, summary, disclaimer, part, rev, plan_ok, unmeasurable, path?, size?, format?}; ok=False means at least one characteristic measured out of limits.

add_thumbnailA

Place a small isometric pictorial of the part in the top-right corner of the sheet — the "glance" reference a machinist uses to grok the 3-D shape before reading the orthographic views — IF it fits there without crowding the existing views and dimensions.

It is a real TechDraw isometric projection rendered through the same path as the other views (a vector line drawing, not a raster), scaled to fit a reserved top-right box and pinned to that corner (fit_page leaves it put, and it is never dimensioned or counted by the manufacturability gate). Best-effort: when the top-right corner is already occupied it returns {placed: False, reason} rather than overlapping content. Call it AFTER placing the views and dimensions (and after fit_page) so "fits" is judged against the final layout.

Returns {placed, box, scale?, view?, reason?}.

add_section_viewA

Add a cross-section view when the part has internal features the outline / hidden-line views convey ambiguously — a counterbore, a blind hole/bore, or a pocket. The need is judged automatically from the real solid (the same feature enumeration the manufacturability gate uses); the cut runs lengthwise through such a feature so its bore profile and depth read directly, and the view is placed in clear space beside the existing views.

auto (default True): add the section ONLY if the part actually has hidden internal geometry; otherwise return {added: False, recommended: False}. Set auto=False to force a section regardless. process: 'auto' (default) | 'prismatic' | 'turned' — how features are enumerated.

Returns {added, recommended, reasons, feature_ids, view?, normal?, origin?}. (drawing_gate also reports section_recommended so you can decide in advance.)

render_viewB

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}.

render_viewsB

Render multiple views of a single object. Returns {views: {view_name: {png_base64,...}}}. Default views: ['iso', 'top', 'front'].

render_fem_resultsA

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}.

render_photorealA

Photorealistic render of a shaped object via the FreeCAD Render workbench (an external renderer, e.g. POV-Ray) — a presentation-quality "nice picture", unlike render_view's fast software-rasterized preview.

Requires the Render addon and a renderer binary to be installed (see docs/RENDER_WORKBENCH.md); raises with install guidance otherwise. Renders in an isolated temporary document, so the live model is never modified.

view: 'iso' | 'top' | 'bottom' | 'front' | 'back' | 'left' | 'right' | 'side'. material: optional Render material library card — e.g. 'Gold', 'Glass', 'Aluminium', 'GlossyPlastic', 'RoughPlastic', 'Iron', 'Brass'. Omitted gives a neutral default material; an unknown name raises with the full list. Returns {png_base64, png_path, renderer, view, material, width, height}.

Presentation-only: output is not bit-reproducible, so it is kept out of the reliability/golden tests. External renders can take seconds to minutes, so this call uses an extended worker timeout.

render_photoreal_submitA

Start a photorealistic render asynchronously; returns immediately with {job_id, status} instead of blocking for the whole render.

Use this (rather than render_photoreal) for renders that may take a long time — heavy materials/renderers, large images — so the worker stays responsive. The external renderer runs in the background; poll render_job(job_id) until status is 'done' (then it returns the PNG) or 'failed'. Same arguments as render_photoreal; requires the Render addon + a renderer binary (see docs/RENDER_WORKBENCH.md).

render_jobA

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.

render_capabilitiesA

Report which photoreal renderers are usable right now, and whether the FreeCAD Render addon imports — so you can pick a working renderer for render_photoreal instead of discovering availability by trial and error.

Takes no arguments. Resolves each renderer's binary exactly as render_photoreal would (ANKUSDRIVE__PATH env override -> FreeCAD prefs -> PATH -> per-OS install dirs), but renders nothing and changes no settings.

Returns {addon_importable (bool), default_renderer ('Povray'), platform, available (sorted list of ready renderer names for the renderer= argument), renderers: {name: {available, param_key, batch, binaries, and either path (the resolved binary) or install_hint}}, materials (library card names usable as render_photoreal's material= argument, present only when the addon imports), and addon_error (present only when the addon does not import)}.

solve_capabilitiesA

Report which P2 external solvers (CFD/MBD/topology/transient-thermal/optics) are usable right now — so you can pick a working solver for a *_submit family instead of discovering availability by trial and error. The solver twin of render_capabilities.

Takes no arguments. Resolves each solver side-effect-free: a binary by ANKUSDRIVE__PATH env -> PATH -> per-OS install dirs; a pip-wheel solver by importability. It executes nothing and installs nothing.

families[*].any_available is the gate to trust: it means AnkusDrive can actually DRIVE that family here, not just that a binary resolved. A solver that resolves but that no AnkusDrive tool can build a case for is listed under the family's prepared_case_only (with the reason on the solver entry) and does NOT set any_available — today that is SU2, which only ever runs a case_dir you prepared yourself (*.cfg + *.su2); every built-in CFD case mode is OpenFOAM-only.

Returns {platform, available (sorted ready solver names), unwired, prepared_case_only, solvers: {name: {available, kind ('binary'|'wheel'), family, extra, and either path/module (when available) or install_hint, plus prepared_case_only when nothing can build it a case}}, families: {family: {solvers, available, unwired, prepared_case_only, any_available}}, extras: {extra: [solver names]} for pip install ankusdrive[<extra>]}.

setup_statusA

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).

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), solvers: {available, unwired, prepared_case_only, solvers: {name: {..., install_hint | wire_hint}}, families: {family: {solvers, available, unwired, prepared_case_only, any_available}}, extras}} — families[*].any_available is what gates each *_submit family, and every unavailable item carries its own fix string. A solver under prepared_case_only resolves but no AnkusDrive tool can build it a case, so it does not make its family available (SU2/cfd — issue #237).

fem_new_analysisC

Create a Fem::FemAnalysis container. Returns {handle, name}.

fem_set_solverB

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.

fem_set_materialB

Bind a material to a body in an analysis.

material is a dict with at minimum: {"YoungsModulus": "210000 MPa", "PoissonRatio": "0.30", "Density": "7900 kg/m^3", "Name": "Steel-Generic"} Any extra keys are passed through to the FEM material card.

fem_add_constraintC

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.

fem_modalA

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.

fem_modal_resultsB

Extract natural frequencies from a completed modal run. Returns {frequencies_hz: [...], modes: [{mode, frequency_hz, max_displacement_mm}, ...]}.

contact_setupA

Set up surface-to-surface contact between face pairs for a CalculiX solve and flip the solver to nonlinear — no new solver (promotes the CCX contact/nonlinear flags the FEM path already exposes). face_pairs is a list of {a:{handle, tag|face}, b:{handle, tag|face}} (master, slave) pairs; friction is the Coulomb coefficient (0 = frictionless); slope optionally sets the penalty contact stiffness; nonlinear (default True) sets the solver's GeometricalNonlinearity.

Run fem_run + fem_results after. Gate RELATIVE to a bonded reference on the same mesh: a bonded model is stiffer (less peak displacement) than frictional contact. Returns {contacts:[handles], n_pairs, friction, nonlinear}.

fem_set_nonlinear_materialA

Attach an elastoplastic (*PLASTIC) hardening curve to a linear FEM material and switch the CalculiX solve to nonlinear — the material-nonlinearity half of the nonlinear FEM path (contact_setup is the geometric/contact half). No new solver: this promotes the CCX MaterialNonlinearity / GeometricalNonlinearity flags the FEM path already exposes. base_material is the handle from fem_set_material (its YoungsModulus/PoissonRatio stay the elastic branch).

Give the post-yield curve either as yield_points ([[stress_MPa, plastic_strain], ...], first point at plastic_strain 0 = initial yield) or from yield_mpa (+ optional tangent_modulus_mpa linear-hardening slope and max_plastic_strain). With no tangent modulus the curve is elastic–perfectly-plastic and caps the stress at σ_y exactly. hardening: 'isotropic' (monotonic) or 'kinematic' (cyclic/Bauschinger). Set geometric_nonlinearity=true to combine plasticity with large deflection (*NLGEOM). ramp_increments sub-divides the load step so ccx's plastic return-mapping converges. Run fem_run + fem_results after; gate against plastic_collapse (perfectly-plastic stress saturates at σ_y, collapse at M_p).

Returns {handle, name, hardening, yield_points, n_points, solver_material_nonlinear, solver_geometric_nonlinear, ramp_increments}.

fem_bucklingC

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.

fem_buckling_resultsB

Extract buckling load multipliers from a completed buckling run. Returns {buckling_factors: [...], modes: [{mode, factor}, ...]}.

fem_thermal_resultsA

Extract temperature field summary from a completed thermal run. Returns {temperatures_c: {min, max, mean}, top_n_hot_nodes: [...]}.

fem_mesh_refinementA

Add a local mesh-refinement region to an existing FEM mesh.

mesh: handle of the FEM mesh. refs: list of {handle, face|edge|tag} dicts identifying the elements (faces/edges) to refine on. char_length: characteristic element length (mm) on those elements; should be smaller than the global mesh setting to actually refine.

fem_meshB

Create a Gmsh mesh of body, attached to analysis.

char_length: max characteristic element length in mm. 0 = let Gmsh pick. 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}.

fem_runB

Run the CalculiX solver on an analysis. Blocks until the solve finishes. workdir defaults to <TMPDIR>/ankusdrive_fem. Returns {workdir, status}.

fem_resultsB

Extract summary results from an analysis.

Returns {max_vonmises_mpa, max_displacement_mm, max_displacement_vector, top_stress_nodes: [{node, vonmises_mpa, displacement_mm}, ...]}.

fem_result_probeA

Probe FEM results at a specific location, instead of only the global max + top-N that fem_results returns. Answers "what is the stress at this point / on this face?" — the agent-friendly form for design iteration.

Pick exactly one mode:

  • POINT: point=[x, y, z] (mm, model coordinates). Interpolates the field barycentrically inside the tet containing the point (method='interpolated'); if the point is outside the mesh it falls back to the nearest node and reports distance_mm (method='nearest_node').

  • FACE: handle= + face=<'f_*' tag or 'FaceN'>. Aggregates the field over the mesh nodes on that CAD face, returning {min, max, mean}.

field: 'auto' (default — every field present in the result) | 'vonmises' | 'displacement' | 'temperature'.

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?:{...}}.

fem_cantilever_demoA

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.

material_getA

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.

material_selectA

Ashby-style selection: filter the corpus, then rank survivors.

criteria keys are min_/max_ (e.g. min_yield_mpa, max_density_g_cc, min_service_temp_c). rank_by: specific_strength | specific_stiffness | strength | stiffness | cost | density. Returns {rank_by, count, criteria, candidates:[{name, score, yield_mpa, density_g_cc, youngs_gpa, cost_usd_kg}, ...]} best-first; an empty filter returns no candidates rather than the closest miss.

material_listA

List available materials, optionally filtered to one category ('aluminum' | 'steel' | 'titanium' | 'magnesium' | 'polymer' | 'glass'). Returns {count, category, materials:[{name, category}, ...]} sorted by name.

fluid_propsA

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.

bolted_joint_checkA

Rate a bolted joint (VDI 2230-lite). Geometry from bolt_dia_mm (+pitch_mm) OR a standard thread named via bolt_size ("M8"/"M8x1.0") that pulls dia/pitch and the standards-table tensile stress area; proof strength from the ISO 898-1 property_class ("8.8") when given. Preload from torque via T=KFd (pass torque_nm OR preload_n). Returns {preload_n, tensile_stress_area_mm2, bolt_stress_mpa, preload_pct_proof, bolt_stress_with_load_mpa, separation_load_n, separation_margin, pass, governing}. preload_target_pct is the fraction of proof strength the preload is judged against (0.75 default; 0.65 for a reused bolt, 0.90 for a critical permanent joint).

bearing_lifeA

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).

spring_checkA

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.

gear_ratingA

Rate spur-gear tooth bending (Lewis): sigma=Ft/(bmY). Pass tangential_force_n, or power_w + pinion_speed_rpm. Returns {tangential_force_n, pitch_dia_mm, pitch_line_velocity_m_s, lewis_form_factor, bending_stress_mpa, allowable_bending_mpa, bending_sf, pass}. First-order screen, not full AGMA. The allowable defaults to the material's fatigue endurance (else 0.3·UTS); allowable_bending_mpa overrides it with an AGMA/spec number.

belt_driveB

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}.

press_fit_stressA

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}.

seal_checkA

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}.

chain_driveA

Rate an ANSI roller-chain drive (ASME B29.1). Rated at the lower of the link-plate-fatigue (HP1=0.004N1^1.08n1^0.9P^(3-0.07P), low speed) and roller-impact (HP2=1000KrN1^1.5P^0.8/n1^1.5, high speed) envelopes, P in inches. Give chain_pitch_mm (matching add_sprocket) OR a chain_number ("40","60",...) for pitch+Kr; strands scale by the B29.1 factor. Powers in W. Returns {rated_power_w, type1_power_w, type2_power_w, governing, strands, strand_factor, ..., power_sf?, pass}. k_r overrides the roller-impact constant the chain_number lookup supplies (29 for the 25-240 series, 17 for the lightweight #41) — needed for a chain outside the ANSI table.

weld_groupA

Rate a planar fillet-weld group by Blodgett's treat-weld-as-a-line method. segments=[((x1,y1),(x2,y2)),...] (mm); an in-plane force_n=[Fx,Fy] at load_point_mm=[px,py] gives direct shear f=F/L plus torsional f=Tr/J from the eccentric moment about the weld centroid, added vectorially at the worst end. required_leg = f_r/(0.707allowable); given leg_mm, throat stress f_r/(0.707*leg) is checked vs allowable. Returns {weld_length_mm, centroid_mm, Ix_mm3, Iy_mm3, J_mm3, direct_shear_n_per_mm, max_shear_n_per_mm, worst_point_mm, required_leg_mm, throat_stress_mpa?, shear_sf?, pass}. weld_type ('fillet' default) labels the group and selects the throat convention.

tolerance_stackupA

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.

fit_checkA

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).

fit_classA

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).

gdt_checkA

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}.

fatigue_checkA

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).

fracture_checkA

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.

wear_estimateA

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}.

creep_flagA

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}.

h_estimateA

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}.

acoustic_screenA

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}.

plate_checkA

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}.

beam_bucklingA

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}.

plastic_collapseA

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}.

elastica_deflectionA

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}.

hertz_contactA

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}.

laminate_propertiesA

Effective stiffness, thermal warp, and first-ply failure of a bonded multi-layer composite stack (NO solver) — the closed-form screening twin a layered multi-material CalculiX fem_run static solve is gated against (e.g. 1 metal layer + 1 plastic layer, or any [(material, thickness), …]).

layers is the stack bottom→top; each entry is a mapping with a thickness (mm) and either a corpus material name or explicit E/youngs_mpa/ youngs_gpa (+ optional nu/poisson, yield_mpa, cte/cte_per_k, density_kg_m3, thermal_conductivity_w_mk); explicit values override the card. width_mm scales EI / first-ply. Optional delta_T (K) gives the bimetal thermal curvature; force_n (in-plane, total across width) and/or moment_nmm (about the neutral axis) give the first-ply margin.

Computes the in-plane modulus (Voigt rule-of-mixtures parallel, Reuss series through-thickness); the transformed-section neutral axis, EI_eff, and flexural modulus E_flex = 12·EI/(b·h³); the CLT A/B/D matrices per unit width (B ≠ 0 ⇒ bending–extension coupling / warp warning); mass-averaged ρ, stiffness-weighted in-plane CTE, series/parallel thermal conductivity; the transformed-section bimetal curvature (= Timoshenko's two-layer formula exactly, also reported); and per-layer extreme-fibre stress → margin to yield → governing layer + load to first yield. A single-material stack reduces to that material's E / EI; a symmetric stack gives B = 0; ΔT = 0 or zero CTE-mismatch gives zero curl. Escalate to a layered fem_run solve for thick stacks, anticlastic curvature, free-edge interlaminar stress, or non-isotropic plies.

Returns {n_layers, width_mm, total_thickness_mm, layers, E_inplane_mpa, E_through_mpa, neutral_axis_mm, EI_eff_nmm2, E_flex_mpa, A_matrix, B_matrix, D_matrix, coupling_ratio, asymmetric, rho_eff_kg_m3, cte_eff_per_k, k_through_w_mk, k_inplane_w_mk, delta_T, thermal_curvature_per_mm, radius_of_curvature_mm, timoshenko_curvature_per_mm, applied_force_n, applied_moment_nmm, kappa_applied_per_mm, axial_strain, layer_stresses, first_ply, fidelity, band_pct, valid_range_ok, warnings, escalate_to}.

monopole_sphereA

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}.

rigid_sphere_scatteringA

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}.

acoustic_radiation_submitA

Exterior-acoustics boundary-element solve on Bempp, asynchronous (OFF the MCP channel) — the real-field twin of the analytic monopole_sphere / rigid_sphere_scattering oracles. Bempp is MIT but needs meshio>=4 (clashing with solidspy's meshio==3 in the shared venv), so it is run ONLY out-of-process via ankusdrive/bempp_runner.py under a dedicated .venv-bempp; degrades to {ok:false, reason, install} when no bempp venv resolves.

problem='radiation' (default): a pulsating (monopole) sphere of radius a_m, uniform surface velocity u_amp at freq_hz, into air (rho,c), mesh size h (fraction of a). The result's radiated_power_w / farfield_pressure_x_r vs the monopole_sphere oracle (ratio≈1) IS the gate. problem='scattering': a rigid sphere insonified by a unit plane wave; sweep ka_list, report the far-field form function at theta_deg angles (h_per_wl elements/wavelength) — gated against the rigid_sphere_scattering Mie oracle. problem='mesh_solve': a radiation solve on a REAL FreeCAD model (handle), tessellated to a surface mesh here and fed to the BEM engine (scale_to_m mm→m, linear_deflection mesh tolerance).

Returns the degradation dict, or {job_id, status, cache_hit}; poll job_result for {ok, ka, radiated_power_w, farfield_pressure_x_r, surface_pressure_abs_mean, n_elements, wall_s} (radiation/mesh_solve) or {results:[{ka, form_function_abs{}, backscatter_abs, n_elements}]} (scattering).

waveguide_cutoffA

Exact rectangular-waveguide cutoff frequency (NO solver) — the closed-form twin the openEMS FDTD full-wave solve (em_fullwave_submit) is gated against. Broad wall a_mm, narrow wall b_mm (default a/2, WR convention). mode is 'TE'/'TM'. f_c(m,n) = (c/2√εᵣ)·√((m/a)²+(n/b)²); dominant TE10 reduces to the EXACT f_c = c/(2a√εᵣ). Below f_c the guide is evanescent (axial β imaginary, nothing transmits), above it propagates with guided wavelength λ_g = 2π/β. With a probe freq_ghz the regime (propagating / evanescent), k, β and λ_g are returned. An FDTD drive straddling f_c must collapse its transmission below the analytic cutoff and rise above it.

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, guided_wavelength_mm, fidelity, band_pct, valid_range_ok, warnings, escalate_to}.

dipole_resonanceA

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}.

em_fullwave_submitA

Full-wave FDTD EM solve on openEMS, asynchronous (OFF the MCP channel) — the real-field twin of the analytic waveguide_cutoff / dipole_resonance oracles. openEMS is GPL-3.0 and is run ONLY out-of-process via ankusdrive/em_fullwave_gpl_runner.py; degrades to {ok:false, reason, install} when no openEMS venv resolves.

problem='waveguide_sweep' (default): hollow rectangular guide, broad wall a_mm/narrow wall b_mm (default a/2), length length_mm (default 60), 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. The result's fc_crossing_ghz (half-power transmission) vs the analytic c/(2a) (fc_ratio≈1, evanescent_mean ≈0, propagating_mean≈1) IS the gate. problem='dipole_s11': centre-fed thin dipole (length_mm, gap_mm, radius_mm), sweep S11, report first resonance.

Returns the degradation dict, or {job_id, status, cache_hit}; poll job_result for {ok, fc_analytic_ghz, freq_ghz[], s21_db[], transmission_norm[], fc_crossing_ghz, fc_ratio, evanescent_mean, propagating_mean, n_cells, wall_s} (waveguide) or {freq_ghz[], s11_db[], resonance_ghz} (dipole).

fsi_plate_deflectionA

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}.

fsi_interface_balanceA

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}.

fsi_channel_pressureA

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}.

fsi_pressure_plate_submitA

Partitioned fluid-structure-interaction solve on the preCICE OpenFOAM↔ CalculiX stack, asynchronous (OFF the MCP channel) — the real coupled-field twin of the analytic fsi_plate_deflection / fsi_interface_balance oracles. A flexible flap clamped at a channel floor deflects under the flow: OpenFOAM (pimpleFoam) writes the wet-interface Force, ccx_preCICE returns the Displacement, and preCICE drives the implicit coupling to convergence each time window. preCICE is LGPL-3.0 and the two heavy solvers run ONLY as subprocesses; degrades to {ok:false, reason, install, stack} when the stack is absent (build via scripts/install-solvers.sh fsi).

Physics knobs: inlet_velocity_m_s, nu_m2_s, rho_kg_m3 (fluid), youngs_pa/poisson/density_kg_m3 (solid), end_time_s/time_window_s/ max_iterations (coupling). Geometry/mesh come from the validated vendored template (no FreeCAD touch). Returns the degradation dict, or {job_id, status, cache_hit}; poll job_result for {ok, time_windows, tip_disp_m, tip_history, coupling_converged, case_dir} — the tip displacement is the field the fsi_plate_deflection oracle gates.

molding_screenA

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}.

moldability_screenA

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}.

drop_impactA

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'; no explicit impact-dynamics solve is shipped (escalate_to=None — horizon scope).

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}.

thermal_lumpedA

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}.

thermal_transient_1dA

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}.

thermal_transient_submitA

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 case).

thermal_radiation_submitA

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).

thermal_composite_wallA

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}.

cht_channel_submitA

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}.

cht_graetz_submitA

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}.

acoustic_fem_submitA

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}.

harmonic_responseA

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}.

harmonic_response_submitA

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}.

em_skin_depthA

Exact AC skin depth (NO solver) — δ = √(2/(ω·μ₀·μ_r·σ)) plus the per-square surface resistance R_s = 1/(σ·δ); fields/current decay e^(−x/δ) into the conductor (~95% of induction heating deposits within 1.5·δ). σ from an explicit conductivity_s_m or a conductor name (copper, aluminum, silver, gold, brass, steel-mild, stainless-304).

Returns {skin_depth_m, skin_depth_mm, surface_resistance_ohm, angular_frequency_rad_s, conductivity_s_m, mu_r}.

em_dc_resistanceA

Exact DC resistance of a uniform conductor (NO solver) — R = L/(σ·A), the closed-form anchor the Elmer em_conduction_submit gate reproduces to machine precision. With voltage_v the Ohm/Joule pair is included (I = V/R, P = V·I). σ from conductivity_s_m or a conductor name.

Returns {resistance_ohm, conductivity_s_m, length_m, area_m2, current_a?, joule_w?}.

em_fieldA

Exact magnetostatic field of the two canonical sources (NO solver): kind='wire' is the long straight wire B = μ₀·I/(2π·r) at distance_mm (Ampère's law); kind='solenoid' is the long-solenoid interior B = μ₀·μ_r·n·I with turns_per_m.

Returns {b_t, b_mt, …} (the field in tesla and millitesla).

em_conduction_submitA

DC current conduction via Elmer's StatCurrentSolver (P3 M6 frontier), asynchronous. Requires ElmerSolver; when absent this returns {ok:false, reason, install} rather than raising. Builds a rectangular strip with voltage_v across its ends and reads the electrode current, total Joule heating and Elmer's effective resistance — all machine-exact against R = L/(σ·A) (resistance_ratio = 1.000000 live).

Returns the degradation dict or {job_id, status, cache_hit}; poll job_result for {ok, current_a, joule_w, effective_resistance_ohm, resistance_exact_ohm, current_exact_a, resistance_ratio, case_dir}.

em_induction_submitA

AC skin effect / induction via Elmer's harmonic 2-D magnetodynamics (P3 M6 frontier), asynchronous. Requires ElmerSolver; when absent this returns {ok:false, reason, install} rather than raising. Builds a conductor slab depths skin depths deep driven by the surface vector potential at frequency_hz, solves the complex field, and fits the e-folding length of BOTH the magnitude and the phase of A(x) — each must equal the exact δ = √(2/(ω·μ₀·μ_r·σ)) (live: decay_ratio 0.999, phase_ratio 1.000). The Joule deposition profile |J|² ∝ e^(−2x/δ) is the induction-heating answer.

Returns the degradation dict or {job_id, status, cache_hit}; poll job_result for {ok, skin_depth_exact_m, decay_length_m, phase_length_m, decay_ratio, phase_ratio, case_dir}.

em_induction_heating_submitA

Coupled induction heating via Elmer (SIMULATION_NEXT B5), asynchronous — completes em_induction_submit into a THERMAL answer: the harmonic MagnetoDynamics solve runs once, MagnetoDynamicsCalcFields turns it into the time-averaged Joule loss field, and a transient adiabatic HeatSolver integrates it for heat_duration_s. Requires ElmerSolver; when absent this returns {ok:false, reason, install} rather than raising.

Two gates: joule_power_ratio — the solved eddy-current power vs the exact deep-slab dissipation P″ = R_s·|H₀|²/2 = ω²σA₀²δ/4 (from the shipped em_skin_depth chain; live 1.0003) — and energy_balance_ratio — the mean temperature rise vs P·t/(m·cₚ) (live 1.005). Conductor σ from a name or explicit conductivity_s_m; thermal ρ/cₚ/k explicit. Also accepts a prepared case_dir.

Returns the degradation dict or {job_id, status, cache_hit}; poll job_result for {ok, eddy_power_w_m, p_total_exact_w_m, joule_power_ratio (≈1), t_mean_final_k, dt_mean_exact_k, energy_balance_ratio (≈1), skin_depth_m, case_dir}.

cfd_pipe_flowA

Analytic straight-pipe pressure drop (NO solver) — the fast internal-flow screen and the exact gate the OpenFOAM cfd_internal_flow solve is checked against. Laminar (Re<2300) is Hagen–Poiseuille Δp = 128·μ·L·Q/(π·D⁴) with its D⁴ scaling — exact; turbulent uses smooth-pipe Blasius, or Colebrook–White when roughness_mm is given (the Colebrook value is always reported for turbulent flow) — a ±10 % Moody-band correlation (fidelity labeled). Give flow as flow_rate_lpm or velocity_m_s; fluid μ,ρ from a name ('water-20c','air-20c','oil-sae30-20c','glycerin-20c') or explicit mu_pa_s+rho_kg_m3. Escalate turbulent cases to cfd_internal_flow_submit(turbulence='kOmegaSST').

Returns {reynolds, regime, velocity_m_s, flow_rate_m3_s, friction_factor, colebrook_friction_factor, relative_roughness, pressure_drop_pa, wall_shear_pa, hagen_poiseuille_pa, laminar, fidelity, band_pct, escalate_to}.

cfd_internal_flow_submitA

Internal-flow CFD (pressure drop) via OpenFOAM or SU2, asynchronous. Requires an OpenFOAM (apt/conda) or SU2 binary; when none resolves this returns {ok:false, reason, install} rather than raising. The pipe and geometry-bridge modes need OpenFOAM specifically — they emit OpenFOAM dictionaries — but channel_height_mm builds a NATIVE SU2 case, which is why solve_capabilities counts SU2 toward the cfd family (issue #237). On Apple Silicon that is the difference between needing a Multipass VM and not. turbulence='kOmegaSST' upgrades the pipe validation case to RANS (SIMULATION_NEXT B3): wall-function k/ω/ν_t with first-cell y+ targeted at ~30–100, developed dp/dx fitted over the second half of a ≥40·D pipe, gated BANDED against Colebrook (colebrook_ratio ≈ 1 ± 10 % — the Moody correlation is itself a band, never an exact gate). Four modes:

  • Build the native SU2 plane-channel case (no OpenFOAM, no VM): pass channel_height_mm, optionally channel_length_mm (default 10× the height), velocity_m_s (default Re 50), a fluid or mu_pa_s+rho_kg_m3 (default a light oil — holding Re low with water means millimetres per second, where SU2's incompressible pseudo-time is badly scaled), nx/ny, max_iterations. Gated against the EXACT plane-Poiseuille closed form Δp = 12·μ·U·L/h², returning poiseuille_ratio ≈ 1. The inlet is the fully developed parabolic profile, so there is no entrance-length error to drown out with a long domain.

  • Build the straight-pipe validation case (no case prep): pass diameter_mm, length_mm, and velocity_m_s (or flow_rate_lpm), with a fluid name or explicit mu_pa_s+rho_kg_m3 (mesh density via n_axial/n_radial, iterations via end_time). The handler builds the axisymmetric laminar pipe, runs blockMesh+simpleFoam, and returns the solved Δp next to the Hagen–Poiseuille analytic reference (cfd_pipe_flow) — the kickoff's exact CFD gate, hp_ratio≈1.

  • Solve a real FreeCAD solid — the geometry bridge: pass a body handle plus inlet_face/outlet_face (1-based indices into the solid's faces; every other face becomes a no-slip wall) and velocity_m_s (applied along the inlet face's inward normal). The solid tessellates into a multi-region STL and meshes with blockMesh + snappyHexMesh; base_cell_mm sets the background cell size, location_in_mesh_mm the kept-region seed point (default: bbox centre — set it for non-convex solids), stl_tolerance_mm the tessellation sag. Use the developed-profile pressure_drop_pa; also pass diameter_mm+length_mm to get an hp_ratio reference for pipe-like bodies.

  • Run a prepared case_dir (optionally an application, e.g. 'simpleFoam'/'foamRun'); for OpenFOAM its environment is sourced before the run, while an SU2 case (*.cfg + *.su2 mesh) runs as a direct native subprocess — no bash/WSL needed, including on Windows.

Returns the degradation dict, or {job_id, status, cache_hit}; poll job_result. Pipe/body cases: {ok, returncode, pressure_drop_pa (developed), pressure_drop_inlet_pa, hagen_poiseuille_pa?, hp_ratio?, n_cells, case_dir}; RANS pipe adds {dpdx_pa_m, dpdx_colebrook_pa_m, colebrook_ratio, y_plus_estimate, band_pct}. Prepared case: {ok, returncode, solver, application, case_dir, kind, stdout_tail}.

declare_performanceA

Record a quantitative PERFORMANCE spec on a part so it can be re-proved after every edit — the performance twin of declare_intent, which only covers geometry. Persists in the .FCStd as a JSON property bag; one contract per part, re-declaring replaces it.

Each requirement is metric-agnostic — the contract layer only orchestrates, so the metric is whatever the named tool already returns, and "Cd ≤ 0.30", "Δp ≤ 50 Pa", "first mode ≥ 200 Hz" and "ΔT ≤ 40 K" are the same machinery:

{"name": "drag_at_cruise",
 "metric": "cd",                        # dotted path into the tool's result
 "tool": "cfd_external_flow_submit",    # what measures it at solver tier
 "conditions": {"model": "$handle", "velocity_m_s": 30, "fluid": "air-20c"},
 "limit": {"max": 0.30},                # max, min, or both (a window)
 "screen": {"tool": "cfd_body_drag", "metric": "cd",
            "conditions": {"shape": "sphere", "diameter_mm": 50,
                           "velocity_m_s": 30}},
 "fidelity_floor": "solver",            # "screen" if an estimate is proof enough
 "trust": {"converged": true, "band_max_pct": 5}}

"$handle" anywhere in conditions is replaced with this part's handle at verification time, so a contract is portable between parts. trust demands are enforced by verify_performance against the solver's own trust block: a requirement asking for converged: true can never be satisfied by an unconverged solve.

Returns {handle, contract: {requirements: [...]}, n_requirements}. Raises ValueError on a malformed requirement, naming the offending one.

verify_performanceA

Prove (or fail to prove) every requirement declared with declare_performance — the step that turns "a solver printed 0.29" into a claim with a band and a provenance.

A verdict has THREE states. pass and fail each require the measurement's whole uncertainty band to sit on one side of the limit; a band that straddles it is indeterminate, meaning "escalate", not "probably fine". A correlation reading Cd = 0.28 ± 10 % against a limit of 0.30 spans 0.252–0.308 and has NOT shown the part passes — collapsing that to a pass is how a spec silently goes unmet.

tier picks the evidence:

  • 'screen' — each requirement's cheap estimator only. Milliseconds, no solver.

  • 'solver' — the real solve for every requirement.

  • 'auto' (default) — screen first, escalate only what the screen could not decide or what declares fidelity_floor: 'solver'. This is the ladder that keeps a design loop cheap: cheap measurements eliminate candidates, solves confirm survivors.

Trust is part of the measurement, not a footnote: trust: {converged: true} or a band_max_pct cap makes an unconverged (or insufficiently mesh-converged) solve come back indeterminate with the reason, never pass.

Solver-tier measurements are asynchronous, so this returns EITHER the finished verdict (screen-only, or everything already decided) or {job_id, status, pending, results} — poll job_result for the completed verdict. Never raises on a failing requirement; a failure is a row.

Every verdict is also RECORDED on the part, stamped with a geometry signature of the shape it measured (#261). That record is what merge_assembly, substitutability_check and component_contract_check consult, since a gate has to answer synchronously and this may not have: an in-flight solve records rows the gates read as unverified, and editing the part invalidates the signature so they read stale — never a pass on either path.

Returns {handle, tier, ok, n_requirements, passed, failed, indeterminate, escalate, results: [{name, tier, metric, state, measured, limit, band_pct, worst_case, best_case, margin, margin_pct, detail, trust_reasons?, screen?, job_id?}]}.

study_submitA

Sweep parameters over a sampled design space and record the WHOLE search as a table — the DOE primitive between the parametric layer and the solver catalog.

Without it, exploring a design space means hand-rolling recipe(params) -> solve -> mutate -> repeat and keeping only the last point, so nobody can tell afterwards whether the design is good or merely the one you stopped on. A study keeps every point, with the parameters that produced it.

variables declares the space; each entry is either explicit levels or a range:

  • {"name": "diameter_mm", "values": [8, 10, 12]} — these and only these

  • {"name": "diameter_mm", "min": 8, "max": 12, "levels": 3} — evenly spaced

sampling picks how they combine:

  • {"method": "grid"} (default) — full factorial. Exhaustive, and the only thing that can prove a trend, but it is the PRODUCT of the level counts: three variables at five levels is 125 evaluations.

  • {"method": "lhs", "n_samples": 20, "seed": 0} — Latin hypercube. Each variable's range is cut into n_samples strata and every stratum used once, so cost is decoupled from dimensionality: 20 points cover 6 variables as well as 2. Use it past 2-3 variables. Both are deterministic from seed, which is what makes a re-run hit the cache.

responses says how to measure each point, in the same mapping a performance requirement uses: {"name": "dp", "tool": "cfd_pipe_flow", "metric": "pressure_drop_pa", "conditions": {"diameter_mm": "$diameter_mm", "length_mm": 200}}. Inside conditions, "$<variable>" is that point's value and "$handle" is the part it built; an unknown $token is refused up front, because a sweep that silently measured a literal string at every point returns a flat, plausible, wrong table.

tool can be ANY AnkusDrive tool, including verify_performance — and that is the interesting case. A response that is a contract verdict carries a band, a trust block and a pass/fail/indeterminate state, so points stay comparable across fidelity tiers instead of being bare floats of unknown quality.

recipe (+ fixed_inputs) rebuilds geometry per point; omit it and pass handle (or nothing) to sweep analysis parameters against fixed geometry. Screening-tier responses evaluate inline in milliseconds, so thousand-point studies are viable; solver-tier responses fan out concurrently and one collector job joins them.

Caching IS resumability: identical points hash to the same job content key, so re-submitting a study after a crash, or widening its grid, re-runs only what is new — n_cached is what tells you the re-run was free. max_points (default 64) refuses a sweep larger than you probably meant.

objective{"response": "dp", "sense": "min"} — additionally reports best.

Returns EITHER the finished table or {job_id, status, points, pending}; poll job_result for the completed table. A point that failed to build or measure is a row with ok: false, never an exception. Result: {ok, n_points, n_evaluated, n_cached, n_failed, sampling, variables, points: [{index, params, handle?, ok, responses: {name: {ok, value, band_pct?, converged?, state?, job_id?, cache_hit?, detail?}}, warnings}], responses: {name: {n, n_missing, min, max, mean, argmin, argmax}}, best?}.

optimize_submitA

Vary parameters until the spec is met, then say whether it was PROVEN — the step that closes the design-to-spec loop.

Everything else in this family measures; this searches. Two things make it different from a generic minimizer, and both come from the performance-contract layer underneath it:

A constraint verdict has three states. indeterminate — the measurement's uncertainty band straddles the limit — is NOT a failed step. An optimizer that reads it as a failure walks away from good designs; one that reads it as a pass converges on unproven ones. During the search an indeterminate constraint is scored on its nominal value, so it neither attracts nor repels, and the winner is proved properly at the end.

Convergence is not proof. A simplex can settle on a point that clears its limit by 2 % while its own grid-convergence band is 5 % wide — noise with a favourable sign. proven is therefore reported separately from converged, and is True only when the final measurement has every constraint at pass and no margin swallowed by its own band.

variables must be continuous and bounded — {"name": "diameter_mm", "min": 5, "max": 25, "start": 10}. An optimizer without a box walks to values that satisfy the arithmetic and mean nothing physically.

objective and each constraints entry use the performance-requirement mapping ({tool, metric, conditions, limit, screen, band_pct}), with "$<variable>" in conditions carrying the candidate's value. tier='auto' searches cheaply on each block's screen estimator, then polishes on the real tool from where the screen landed; 'screen' or 'solver' runs just that leg.

The search is a bounded Nelder-Mead — derivative-free because there is no adjoint through a CFD solve — so every evaluation is a real measurement. budget ({max_evals, max_wall_s}, default 40 evaluations) is the ceiling; revisited points are served from cache and do NOT count against it.

Shape optimization. Pass a recipe (+ fixed_inputs) and it is rebuilt for every candidate, so the search varies GEOMETRY rather than only numbers — "$handle" in a response's conditions is that candidate's part, and the returned history carries the handle each point built. Pass handle instead to optimize parameters against one fixed part; pass neither and the search is purely parametric.

Geometry-driven candidates are built on the MAIN thread through the worker's work queue, because FreeCAD's document API is not thread-safe and this search runs as a background job. That queue is drained once per incoming request, so a shape search only advances while you are polling job_status/job_result — the poll you must do anyway is what gives it its turn. Poll at your normal cadence and it simply works; stop polling and it stalls rather than finishing in the background. A candidate whose recipe fails to build is scored out as an infeasible point, not an error. study_submit remains the right tool for a FIXED grid over recipe geometry, which needs no queue at all.

Returns {job_id, status}; poll job_result for {ok, proven, stop_reason, best_params, best_value, objective: {name, metric, sense, value, band_pct}, constraints: [{name, state, measured, limit, band_pct, margin, margin_pct, detail, trust_reasons?}], phases: [{tier, n_evals, best_params, best_value, converged, reason}], history: [{i, tier, params, value, score, feasible, cached}], n_evals, n_cached, budget, variables, warnings}.

grid_convergenceA

Grid Convergence Index — how much of a solved number is the MESH (no solver, milliseconds). Give it the same quantity solved on 2-3 systematically refined meshes, FINEST FIRST, and it fits the observed order of convergence, Richardson-extrapolates to zero cell size, and returns the percentage band inside which the mesh-independent answer lies. Roache's GCI as codified in ASME V&V 20.

This is the honest band_pct for a result with no analytic oracle — the verification counterpart to every validation ratio in the solver families — and it is deliberately family-agnostic: three CFD drag coefficients, three FEM peak stresses and three modal frequencies are all valid input; only you know what the mesh size means. cfd_mesh_independence_submit is the driver that produces the three CFD values for you.

Describe the meshes with either cell_sizes (representative cell length, same order as values) or cell_counts (total cells; h = N^(-1/dimensions)). With THREE values the order is measured and the safety factor is 1.25; with TWO it must be assumed (assumed_order, default 2.0) and the factor triples to 3.0 — a much wider band, which is the honest price of the missing mesh.

Watch three fields before quoting the band: monotonic False means the solutions oscillate and the extrapolation is not meaningful (usually an unconverged level, not a mesh effect); asymptotic_ratio far from 1 means the meshes have not reached the range where the theory holds, so gci_pct is a LOWER bound; order_clamped True means the fitted order was unphysical and a clamped one was used.

Returns {n_levels, values, cell_sizes, refinement_ratios, observed_order, order_used, order_clamped, extrapolated_value, gci_pct, gci_coarse_pct, band_pct, relative_error_pct, monotonic, asymptotic_ratio, safety_factor, converged_fit, fidelity, warnings}.

cfd_mesh_independence_submitA

Solve the same CFD case at 2-3 refined meshes and report the Grid Convergence Index — asynchronous, one job for the whole ladder. Requires OpenFOAM; degrades to {ok:false, reason, install} when it does not resolve.

Answers the question no single solve can: is this number a property of the flow, or of the mesh? On geometry with no analytic twin that band is the only error bar available, and it is what turns "a solver produced 0.31" into "0.31 ± 2 %".

Two families, dispatched like their single-solve twins:

  • the wind tunnel — pass model (or body) + velocity_m_s, plus any cfd_external_flow_submit knob. The ladder varies the background cell; the body is tessellated ONCE and shared, so the study isolates mesh error instead of mixing in a changing STL. Default metric 'cd'.

  • the straight pipe — pass diameter_mm, length_mm, velocity_m_s (or flow_rate_lpm). The ladder scales n_axial/n_radial. Default metric 'pressure_drop_pa'.

The COARSEST level is the mesh a plain submit would have built and the study refines from there (the tunnel's default cell is already the coarsest that resolves the body at all). Cost therefore grows as the cube of refinement_ratio: 3 levels at 1.5 puts roughly 11x the cells in the finest mesh, so budget accordingly. end_time is the cap for the COARSEST level and is scaled up for the finer ones — a fine mesh needs proportionally more sweeps, and a study whose finest level quietly stopped at its cap is worthless.

Returns the degradation dict, or {job_id, status, cache_hit}; poll job_result for {ok, family, metric, levels: [{label, value, cell_size_m, n_cells, converged, returncode, case_dir}], grid_convergence: {observed_order, extrapolated_value, gci_pct, monotonic, asymptotic_ratio, order_clamped, warnings, ...}, band_pct, extrapolated_value, hagen_poiseuille_pa (pipe family), warnings}.

cfd_body_dragA

Analytic EXTERNAL-flow drag screen (NO solver, milliseconds) — the external twin of cfd_pipe_flow, and the banded oracle the wind-tunnel solve cfd_external_flow_submit(model=…) is checked against. Use this FIRST to narrow a design space; escalate to the solve only for the shapes that survive.

Three families:

  • shape='sphere' — Clift–Gauvin over the whole standard drag curve, Cd = 24/Re·(1+0.15·Re^0.687) + 0.42/(1+4.25e4·Re^-1.16). Collapses to the EXACT Stokes 24/Re as Re→0; valid to Re=2e5 (it does not model the drag crisis). Pass diameter_mm + velocity_m_s.

  • shape='cylinder' — Sucker–Brauer crossflow Cd (axis ⟂ flow), Cd ≈ 10 at Re=1, 1.45 at Re=100, 1.2 at Re=1e5. Pass diameter_mm, velocity_m_s, optional length_mm (default 1 m, i.e. drag per unit span; L/D<10 warns about end relief).

  • a tabulated bluff/streamlined shape — 'cube_face_on', 'flat_plate_normal', 'hemisphere_open_back', 'streamlined_body', 'car_modern', … (shape='list' returns the whole table). Needs frontal_area_mm2 (or a model handle, whose silhouette along flow_direction is measured off the live solid) + velocity_m_s; cd overrides the table with a known value.

Fidelity: sphere/cylinder are correlations with band_pct 10/15; the table is band_pct 20 and only valid for Re ≈ 1e4–1e6 on a shape that genuinely matches.

Returns {cd, drag_force_n, frontal_area_m2, dynamic_pressure_pa, velocity_m_s, fidelity, band_pct, escalate_to} plus {reynolds, regime, valid_range_ok, warnings} for sphere/cylinder — or {shapes: {name: cd}} for shape='list'.

cfd_external_flow_submitA

External-flow CFD (drag/lift) via OpenFOAM or SU2, asynchronous. Requires an OpenFOAM (apt/conda) or SU2 binary; when none resolves this returns {ok:false, reason, install} rather than raising. The two case-BUILDING modes below need OpenFOAM specifically — they emit OpenFOAM dictionaries; SU2 only ever runs a case_dir you prepared yourself, which is why solve_capabilities does not count it toward the cfd family's any_available (issue #237). Three modes:

  • Put a real solid in the virtual wind tunnel (issue #223): pass a model (or body) handle + velocity_m_s. The solid's faces tessellate into an STL, a farfield box is auto-sized around it by standard practice (5L upstream / 10L downstream / 5L lateral, overridable via upstream_factor/downstream_factor/ lateral_factor; the reported blockage_ratio warns past 5 %), snappyHexMesh carves the body out, and the forces function object integrates pressure + viscous traction over it. Cd/Cl/Cm come back on the MEASURED frontal silhouette along flow_direction (default +x; exact for a convex body — override with frontal_area_mm2 for a re-entrant one) and reference_length_mm (default: the largest bbox dimension). Mesh knobs: base_cell_mm (default L/2 — a coarser cell is REJECTED, since snappy would then mesh an empty tunnel and report ~0 drag), surface_refine [min,max] levels, wake_refine, stl_tolerance_mm, end_time. Trust: the laminar path is gated live against the sphere drag curve at Re=1 and Re=100 (within ~2 %); turbulence='kOmegaSST' runs but has no verified oracle for arbitrary bodies and comes back gated:false. Past Re≈1000 a laminar request is flagged in warnings rather than silently answered.

  • Build the flat-plate validation case (no model): pass velocity_m_s, with optional plate_length_mm (default 100), a fluid name ('air-20c','water-20c',…) or explicit mu_pa_s+rho_kg_m3, and mesh knobs nx_plate/n_y/end_time. THIS MODE SOLVES A FLAT PLATE, never the caller's geometry: a 2-D laminar plate with a clean leading edge (slip→plate→slip, far-field top), whose wall-shear drag is integrated from the converged U field and returned next to the Blasius reference Cf=1.328/√Re_L (blasius_ratio≈1, ~15 %). turbulence='kOmegaSST' upgrades it to RANS (default plate_length 1000 mm so Re_L > transition), gated BANDED against the mixed-transition Cf = 0.074·Re^(−1/5) − A/Re.

  • Run a prepared case_dir (optionally an application); OpenFOAM runs with its environment sourced, an SU2 case (*.cfg + *.su2 mesh) runs as a direct native subprocess — no bash/WSL needed, including on Windows.

Returns the degradation dict, or {job_id, status, cache_hit}; poll job_result. Body mode: {ok, returncode, cd, cl, cm, drag_force_n, drag_pressure_n, drag_viscous_n, lift_force_n, force_total_n, moment_total_nm, force_drift_pct, n_force_samples, reynolds, reference_length_m, frontal_area_m2, frontal_area_source, moment_reference_m (the bbox centre moments are taken about, not the global origin), blockage_ratio, base_cell_m, converged, gated, warnings, case_dir}. Flat plate: {ok, returncode, reynolds_l, cd, cf_solved, cf_blasius, blasius_ratio, drag_force_n, drag_momentum_n, drag_blasius_n, n_cells, case_dir}; RANS plate swaps the gate fields for {cf_solved (momentum), cf_mixed_ref, cf_mixed_ratio, cf_turbulent_ref, cf_wall_corrected, y_plus_estimate, band_pct}. Prepared case: {ok, returncode, solver, application, case_dir, kind, stdout_tail}.

molding_fill_submitA

Injection-molding FILL (+ optional PACK/COOL) solve, asynchronous — the higher- fidelity twin molding_screen escalates to. Answers can this geometry actually be molded: short-shot / fill ability (the strongest, most reliable gate), fill time, and a peak injection-pressure proxy — a real two-phase (melt + air) flow solve, not the spiral-flow correlation. Requires an OpenFOAM binary; when none resolves this returns {ok:false, reason, install} rather than raising.

Backend: when the openInjMoldSim binary resolves (GPL-3.0, a modified compressibleInterFoam on OpenFOAM-7 .org — Cross-WLF + 2-domain Tait), the worker generates and runs an OF7-org case from the params below (or runs a prepared case_dir if given) — a pressure-driven, non-isothermal plaque fill with the Cross-WLF/Tait coefficients pulled from the materials corpus for resin. Where that build is absent it falls back to a 2-D plaque-cavity interFoam VOF case on the existing OpenFOAM (.com/ESI) — same physics family, answers fill/short-shot but not packing. The GPL solver is held at the subprocess boundary (never imported).

openInjMoldSim (OF7) params: resin (corpus key, default "PS"), length_mm, wall_thickness_mm (gap), depth_mm, nx/ny, peak_pressure_mpa (gate ramp, default 2), melt_temp_c (220), mold_temp_c (60), wall_h_w_m2k (wall heat- transfer coeff; default ~adiabatic for a clean fill — raise for freeze-off), fill_end (terminate fraction, default 0.98). Pass application to force the interFoam-prepared path.

Packing/cooling (openInjMoldSim only): set stages="fill_pack" to also run the cooling continuation after fill (seal the gate, switch walls to cooling, hold). Knobs: pack_phases (default 2), cool_window_s (cooling duration; default a few× the fill time — note a 1 mm wall cools in ~seconds, so a short window gives partial cooling), eject_temp_c (for cooling-time; default 80), pack_wall_h_w_m2k (cool-side wall coeff; default 1250). The pack result adds pack:{rho_mean_final, rho_min, volumetric_shrinkage_pct, frozen_fraction, cooling_time_s, residual_pressure_pa} and pack_gate:{pass (on sink risk), score, volumetric_shrinkage_pct, expected_densification_pct (Tait-EOS), pvt_faithful, sink_risk, warnings}.

Net mold shrinkage (the cavity-sizing number; issue #116) — distinct from pack_gate's raw-PVT densification. The fill_pack result also models the packing-feed make-up (melt fed at the hold pressure until the gate freezes at the Tait no-flow transition; only the uncompensated post-gate-freeze densification is net shrinkage) and gates it against the resin's published linear band. hold_pressure_pa (effective cavity packing pressure; default = the ramp peak), room_temp_c (free-part relax temperature; default 23). Adds net_shrinkage:{net_linear_pct, net_vol_pct, raw_vol_pct, compensated_vol_pct, gate_freeze_temp_k} and shrinkage_gate:{pass (in corpus band), score, net_linear_pct, corpus_band_pct, in_band, warnings}. Plus cooling_dT_through_k — the antisymmetric through-thickness differential auto-derived from the cooling field, ready to flow straight into molding_warpage_submit (pass it, or pass this result's case_dir+nx/ny as cooling_case_dir etc.).

Asymmetric per-wall cooling (#134): set pack_wall_h_low_w_m2k and pack_wall_h_high_w_m2k (the y=0 and y=H mold-face heat-transfer coeffs) to DIFFERENT values to model an asymmetric cool — the case is meshed with split wallLow/wallHigh patches and each face cools at its own rate, freezing a real through-thickness bending differential. This is what makes the live cooling_dT_through_k nonzero (a symmetric cool correctly gives ≈0 → no warp). The part bows toward the slower-cooled (lower-h, hotter, last-to-freeze) face; the result adds asymmetric_cooling:{pack_wall_h_low_w_m2k, pack_wall_h_high_w_m2k, warps_toward}. Leave both unset (or equal) for the default symmetric cool. Feed the resulting case_dir into molding_warpage_submit to predict the warp magnitude.

Drive the interFoam path with cavity + process params:

  • length_mm (flow length, default 100), wall_thickness_mm (cavity height, default 2), depth_mm (out-of-plane, default 1), mesh nx/ny.

  • inject_velocity_m_s OR flow_rate_cm3_s (+ optional gate_height_mm for the gate area) — the melt mean inlet speed.

  • melt rheology: melt_rho_kg_m3 (default 900), melt_nu_m2_s (kinematic, default 1e-3) for Newtonian, or a carreau {nu0,nuInf,k,n} BirdCarreau dict (the shear-thinning Cross-WLF stand-in).

  • end_time_s (run bound; default ≈4× the plug-flow fill time), machine_max_pressure_pa (press limit, default 180 MPa), fill_fraction_pass (full-fill threshold, default 0.97). Or pass a prepared case_dir to run the resolved solver directly.

Returns the degradation dict, or {job_id, status, cache_hit}; poll job_result. interFoam result: {ok, returncode, backend, case_dir, expected_fill_time_s, flow_length_ratio, fill:{filled_fraction, filled_cell_fraction, front_x_frac, last_to_fill_x_frac, max_pressure_pa}, gate:{pass, score, fidelity:"solve", band_pct, short_shot, fill_time_s, max_pressure_pa, pressure_ok, warnings}}.

molding_warpage_submitA

Injection-molding WARPAGE / residual distortion, asynchronous — the FEM thermo-elastic post-step of the cooling solve (GitHub issue #113 Part B; the higher-fidelity twin of the #104 CTE shrinkage screen). Answers will the part bow out of flat once it cools and is ejected. Requires the ccx (CalculiX) binary; when none resolves this returns {ok:false, reason, install} rather than raising.

The physics: a moulding shrinks as it cools (CTE); uniform shrinkage just makes it smaller, but differential shrinkage warps it. The dominant driver is the asymmetric frozen-in through-thickness temperature field at ejection (an unbalanced cooling layout, one mould half hotter, a rib on one face). Pass that as dT_through_k (K, the through-thickness differential: T at the thin-face minimum minus the maximum). The worker meshes body, imposes the field as a thermal eigenstrain, pins a statically-determinate 3-2-1 constraint, and solves the free linear-elastic distortion in ccx. A balanced field (dT_through_k≈0) warps ~0; an asymmetric one bows to the analytic plate curvature (κ=α·ΔT/h), directionally correct.

Backend: CalculiX (ccx), driven by a deck the worker writes directly (the GPL solver is held at the subprocess boundary, never imported). Units mm / MPa / 1/K / °C, so warp comes back in mm.

Coupled cooling hand-off (issue #116): instead of hand-passing dT_through_k, pass cooling_case_dir (a molding_fill_submit(stages="fill_pack") result's case_dir) with cooling_nx/cooling_ny (the cooling case mesh; defaults 60/8) and optional cooling_nz/cooling_time — the worker reads that cooling solve's cell- centre temperature field and auto-derives the antisymmetric (bending) through- thickness differential. The result reports dT_through_k and dT_source.

Params: body (a shape handle — the part), and either dT_through_k or cooling_case_dir (one is required). Material elastic props from material (corpus card) or explicit youngs_mpa/poisson/ cte_per_k — solidified-resin defaults are used with a warning otherwise (the corpus rheology cards don't carry structural props). ref_temp_c is the stress- free / solidification temperature (warp is invariant to it — it only scales the reported residual stress). char_length_mm sets the mesh size; thickness_axis ('x'|'y'|'z') overrides the auto-detected through-thickness axis; flatness_tol_mm or flatness_tol_frac (default 0.2 % of span) set the gate tolerance.

Fidelity caveat: a one-way, linear-elastic, loose coupling — it ignores viscoelastic stress relaxation, flow-induced anisotropy, and the packing-pressure residual; it captures the dominant differential-shrinkage warp and its direction, not a calibrated absolute. fidelity="solve" with a conservative band_pct; read the band.

Returns the degradation dict, or {job_id, status, cache_hit}; poll job_result for {ok, returncode, solver, case_dir, nodes, tets, warp_axis, span_mm, thickness_mm, dT_through_k, dT_source, analytic_bow_mm, gate} where gate is {pass (on flatness), score, fidelity:"solve", band_pct, max_warp_mm, flatness_tol_mm, warp_per_span, max_disp_mm, warp_faithful, analytic_bow_mm, warnings}.

random_vibrationA

Random-vibration response off a modal run (Miles' equation; closed-form, no external solver). Provide either analysis (a handle whose fem_modal + fem_run already produced natural frequencies) or an explicit frequencies_hz list, plus a base-acceleration PSD psd_profile ([{"hz":20,"g2_hz":0.01}, ...]; log-log interpolated, and zero outside its band so a mode stiffened above the band escapes drive). Each mode is an SDOF resonator with amplification q (default 10; rule of thumb Q≈√f_n), combined by SRSS: rms_g = sqrt(Σ (π/2)·f·W(f)·Q). With modal_stress_mpa_per_g the g response converts to RMS / 3-σ stress; add allowable_stress_mpa for a pass/fail.

Returns {rms_g, first_mode_hz, dominant_mode_hz, q, psd_band_hz, miles_grms_g, modes: [{mode, frequency_hz, psd_g2_hz, contribution_g, in_band}], rms_stress_mpa, three_sigma_stress_mpa, pass}.

beam_modalA

Exact Euler-Bernoulli natural frequencies of a uniform rectangular beam — the closed-form modal oracle (no solver), and the band the CalculiX fem_modal eigen-solve is gated against. f_n = (βL)_n²/(2π)·sqrt(E·I/(ρ·A·L⁴)); the beam bends in height_mm (I = width·height³/12, so a slender beam's lowest mode is the thinnest-direction bend). boundary: 'cantilever' | 'simply_supported' | 'clamped_clamped' | 'free_free' | 'clamped_pinned' (up to 5 modes each). Material via youngs_gpa+density_kg_m3, or a Materials-DB material name. Slender-beam theory — accurate while length ≫ height (thick beams need a Timoshenko correction).

Returns {boundary, n_modes, frequencies_hz, beta_l, first_mode_hz, youngs_gpa, density_kg_m3, area_mm2, I_mm4, slenderness}.

dfm_checkA

Screen a part for manufacturability against a pull/tool axis. Give a hand-built faces list of {name, draft_deg, wall_mm?} — draft_deg relative to pull_axis (0 = a vertical wall needing draft; <0 = a re-entrant undercut) — OR a live handle, whose per-face descriptors are read off the solid (draft vs the pull axis + a ray-cast undercut test + inward-chord wall sampling) and scored identically (v2 Shape wiring). draft_violations are 0≤draft<min_draft_deg, undercut_faces are draft<0, min_wall_violations are wall_mm<min_wall_mm (defaults by process: injection 1.0, cnc 0.5, sheet/fdm 0.8).

Sheet metal: a handle built by sheet_base/sheet_flange/sheet_tab/sheet_hem is ALSO screened against the press-brake rules (minimum bend radius by material, minimum flange length, hole-to-bend distance, refold collision) with no extra argument — those rules are DELEGATED to the same implementation sheet_check calls, so the two tools cannot return different verdicts on one part. Pass an explicit sheet block {thickness_mm, material?, bends, holes?, interferences?} to screen bends on a part AnkusDrive did not model.

Returns {process, pull_axis, min_wall_mm, draft_violations, undercut_faces, min_wall_violations, score, pass} — plus n_faces + wall_thickness_stats on the handle path, and a sheet sub-result {ok, findings, rules, fidelity, band_pct} whose failures also gate pass on the sheet-metal path.

optics_raytraceA

Ray-trace a bundle through a dielectric optical model with rayoptics (asynchronous-free; needs no FreeCAD geometry). Requires the rayoptics wheel (the optics extra); when it does not resolve this returns {ok:false, reason, install} rather than raising. The geometric refraction comes from rayoptics; the Fresnel/TIR energy split + the exit histogram come from the exact analysis/optics core, so the trace is gated against that oracle (oracle_max_dev_deg = max rayoptics−Snell exit-angle deviation, ~0).

n_refractive is the medium index n2 (default PMMA 1.49062). source_config is {kind:'collimated'(angle_deg)|'cone'(half_angle_deg)|'lambertian' (max_angle_deg)} (default collimated at normal incidence). model may carry {n1 (incident index, default air 1.0), absorption (0..1 bulk loss), target_half_angle_deg (the acceptance cone counted as efficiency)}.

Returns the degradation dict, or {ok, backend:'rayoptics', rayoptics_version, n_rays, n1, n2, critical_angle_deg, efficiency, leakage_fraction, absorbed_fraction, tir_fraction, energy_balance, oracle_max_dev_deg, exit_distribution:[{angle_deg,intensity}], hotspot_locations}.

optics_lens_designA

First-order + spot analysis of a SEQUENTIAL optical system with optiland (MIT, in-process). Requires the optics extra; degrades to {ok:false, reason, install} otherwise. For a single lens optiland is gated against the analytic thick-lens oracle (oracle_dev_pct).

surfaces: list (object->image) of {radius, thickness, material, stop?} — exactly one surface must set stop:true (the aperture stop). epd (entrance-pupil dia) OR fno. wavelengths_um (first is primary, default [0.5876]). field_angles_deg (default [0.0]). image_solve solves the last gap to paraxial focus.

Returns the degradation dict, or {ok, backend:'optiland', optiland_version, efl_mm, bfl_mm, fno, n_surfaces, rms_spot_um:[per field], oracle_efl_mm, oracle_dev_pct}.

optics_lens_optimizeA

Optimize a SEQUENTIAL optical system with optiland's optimizer (MIT, in-process) — the capability rayoptics lacks. Requires the optics extra; degrades to {ok:false, reason, install} otherwise.

surfaces: as in optics_lens_design. variables: [{type:'radius'|'thickness', surface:<1-based int>}] — the degrees of freedom. targets: [{operand:'f2'| 'rms_spot_size'|…, target, weight?, surface?}] — the merit function. maxiter caps iterations.

Returns the degradation dict, or {ok, backend:'optiland', converged, n_fev, before:{efl_mm,rss}, after:{efl_mm,rss}, surfaces:[optimized]}.

optics_solid_traceA

NON-SEQUENTIAL ray trace through a real solid (STL mesh) with a refractive index — the lane for molded optical parts (light-pipes, prisms, lenses). Backed by KrakenOS, which is GPL-3.0 and is run ONLY in a subprocess (the parent never imports it — same arm's-length isolation as the GPL Elmer/OpenFOAM binaries). Requires the optics_gpl extra; degrades to {ok:false, reason, install} otherwise.

Geometry: pass a model handle (exported to STL here) OR a ready stl_path. Material: glass (KrakenOS catalog name, e.g. 'BK7') or n_refractive (constant index). rays: [{origin:[x,y,z], dir:[l,m,n]}]; each ray's turn_deg is its input->exit bend (~90 for a TIR corner prism, ~0 for a straight pass). solid: {diameter, thickness, axis_move} placement. wavelength_um default 0.55. want_paths (default False): also return each valid ray's polyline as paths — the per-surface hit points [[x,y,z], ...] in the traced frame. The LAST point of each path is the ray's EXIT LOCATION on the solid, so the spatial exit/leakage map a diffuser needs can be reconstructed from it.

Returns the degradation dict, or {ok, backend:'KrakenOS (subprocess-isolated, GPL-3.0)', n_launched, n_valid, valid_fraction, mean_turn_deg, max_turn_deg, rays:[{valid, exit_dir, turn_deg}], stl_path, paths?:[[[x,y,z],...],...]}.

granular_screenA

Closed-form granular/powder-mechanics oracles — banded correlations, NO external solver (the FreeCAD-free analytic twins the YADE DEM solve is gated against). These are correlations, not exact theory, so each returns fidelity='correlation' + an honest [low, high] band; the band IS the oracle. Dispatch on problem:

'packing' (regime='random_close'|'random_loose'|'fcc'[, coordination]) — monodisperse sphere solid-volume fraction φ. RCP ≈ 0.637 (band 0.60–0.66), the random pile a real settle must hit, well below the crystalline FCC/HCP 0.7405. 'beverloo' (outlet_m, particle_d_m[, bulk_density_kg_m3 | material]) — flat-bottom hopper discharge W = C·ρ·√g·(D−k·d)^2.5 [kg/s]; flow ∝ outlet to the 2.5 power, independent of fill height. 'beverloo_exponent' (outlet1_m, flow1_kg_s, outlet2_m, flow2_kg_s [, particle_d_m]) — recover the log-log flow exponent from two (outlet, flow) points; granular 2.5 (band 2.2–2.8) vs Torricelli 2.0. 'repose' (friction_coeff[, saturation]) — poured-pile repose angle θ ≈ atan(μ) + ±25% band. 'repose_monotone' (mu_low, repose_low_deg, mu_high, repose_high_deg) — the steeper-with-friction monotonicity gate.

SI units (m, kg/m³, kg/s, degrees). Escalate to dem_pack_submit / dem_flow_submit (the real YADE solve) for polydisperse mixes, non-spherical grains, cohesion, or geometry this monodisperse idealization can't see.

dem_pack_submitA

Pour N monodisperse spheres into a box and settle them under gravity with the REAL YADE discrete-element engine, then measure the random close-packing fraction φ of the settled bed — gated against granular_screen('packing') (RCP band 0.60–0.66, well below the crystalline 0.7405). YADE is GPL-3.0 and is run ONLY in a subprocess (the parent never imports it — same arm's-length isolation as the GPL Elmer/OpenFOAM binaries). Runs OFF the MCP channel via a background job, so a multi-second settle never blocks the worker.

box_m is the [Lx, Ly] floor footprint (m; default [0.06, 0.06]); the column height is sized to hold n_spheres. friction_deg is the inter-particle friction angle; young_pa the contact modulus; density the grain density. Returns {job_id, status, cache_hit, oracle} (poll job_status / job_result); the job result carries the oracle band PLUS the measured {packing_fraction, in_band, n_settled, settled_height_m, mean_coordination, positions:[[x,y,z,r]]}. Absent YADE: {ok:false, reason, install, oracle}.

dem_flow_submitA

Discharge spheres from a flat-bottomed hopper box through a central orifice with the REAL YADE discrete-element engine and measure the steady mass-flow rate — gated against granular_screen('beverloo') (flow ∝ outlet^2.5). Submit two outlet_m sizes and feed the (outlet, flow) pair to granular_screen('beverloo_exponent') to check the Beverloo 2.5 exponent (vs the Torricelli 2.0 of a draining fluid). YADE is GPL-3.0 and is run ONLY in a subprocess; runs OFF the MCP channel via a background job.

box_m is the [Lx, Ly, Lz] hopper box (m; default [0.10, 0.10, 0.20]); outlet_m the central orifice diameter; settle_steps/flow_steps the DEM step budgets. Returns {job_id, status, cache_hit, oracle}; the job result carries the Beverloo oracle PLUS {mass_flow_kg_s, n_discharged, discharge_time_s, positions:[...]}. Absent YADE: {ok:false, reason, install, oracle}.

optics_moldability_checkA

Moldability screen for a part against a single pull axis — geometric, no solver. Resolves the model handle's solid, then per face computes the draft relative to pull_axis from the outward normal (draft_deg = 90 − angle(normal, pull); 0 = a wall parallel to the pull that needs draft) and ray-casts the face centroid along ±pull: a face the straight pull frees in neither direction is a re-entrant UNDERCUT (reported with negative draft). Inward chords give a wall- thickness distribution. Scored through the same DfM machinery as dfm_check.

pull_axis: '+z'/'-x'/… or an [x,y,z] vector. process ('injection'|'cnc'| 'sheet'|'fdm') sets the default min wall; override with min_wall_mm.

Returns {process, pull_axis, n_faces, undercut_faces, draft_violations, min_wall_violations, wall_thickness_stats:{min_mm,mean_mm,max_mm,n}, score, pass}.

moldability_checkA

Geometry-aware moldability DFx screen — resolves the model handle's solid, samples local wall thickness per face via inward chords (the same machinery as optics_moldability_check), then grades it through the pure-Python moldability screen: WALL-THICKNESS QUALITY (recommended-band range / uniformity / sink risk, cooling tied to the thickest wall) + the CTE SHRINKAGE estimate for the resin. Low-fidelity gate — escalate_to= 'molding_fill_submit'.

material drives the recommended-wall band, the CTE shrinkage, and cooling (degrades gracefully when the corpus lacks the issue #106 fields). nominal_mm anchors the range check (else the sampled-wall mean). The same shrinkage/thickness overrides as moldability_screen apply.

Returns the moldability_screen verdict {thickness:{…}, shrinkage:{…}, pass, score, fidelity, band_pct, warnings, escalate_to} plus {n_faces, n_wall_samples}.

dfa_checkA

Grade an assembly (Boothroyd-Dewhurst-lite). assembly_efficiency = theoretical_min/(part_count+fastener_count) (theoretical_min = unique_part_count or 1); assembly_score scales that by a handling penalty from insertion_axes/ symmetry and decreases monotonically as part/fastener count rises. The grade is an ordinal index for comparing variants (fidelity='correlation', band_pct=None) — rank with it, don't gate on the absolute value. Returns {part_count, fastener_count, insertion_axes, handling_difficulty, assembly_efficiency, assembly_score, symmetry_score, fidelity, band_pct}.

pack_checkA

Check a part against a shipping carton + compute billable weight. part_bbox_mm/carton_mm are [l,w,h] mm; fits allows reorientation (sorted-dim compare). void_fraction = 1−vol(part)/vol(carton); dim_weight_kg = vol(carton cm³)/dim_factor (default 5000 metric DIM); billable_weight_kg = max(actual, dimensional). Returns {fits, void_fraction, dim_weight_kg, actual_mass_kg, billable_weight_kg, pass}.

cost_estimateA

Per-unit cost rollup (Design for Cost). material_cost = volume·density·price ·(1+scrap) from the Materials DB (process: cnc | fdm | casting | injection). process_cost = amortized setup + per-process machine time; tooling amortized over quantity, so unit_cost falls as quantity rises. The machine-time table is order-of-magnitude (fidelity='correlation', band_pct=100) — trust the ratios between processes/quantities, not the absolute dollars; material_cost alone is exact given its inputs.

material may be any Materials-DB card name (material_list / material_get), or anything at all if you price it yourself: price_usd_kg and density_kg_m3 override the DB lookup and are flagged in breakdown.price_basis / density_basis as 'explicit' instead of 'material'. A generic word like 'aluminum' is a CATEGORY, not a card — it carries a density but no price, so it needs price_usd_kg or a real card name (AL6061-T6, …); the error says which.

Two optional inputs sharpen it. tolerance_class ('IT7', '9', …) scales the TABLE machine time by the tolerance-cost curve (see tolerance_cost_check): holding tighter than the process's natural capability roughly doubles cost every 1.5 IT grades. machine_time_hr REPLACES the table with a time a real model computed (cnc_time_estimate) and drops band_pct from 100 to that model's band (50 by default, or machine_time_band_pct); the tolerance factor is then not applied again, because cnc_time_estimate already applied the same curve. Both default to None, reproducing the pre-existing behaviour exactly.

Returns {material_cost, process_cost, tooling_amortized, unit_cost, mass_kg, fidelity, band_pct, breakdown:{…, density_kg_m3, price_usd_kg, density_basis, price_basis, machine_time_hr, machine_time_basis, base_machine_time_hr, tolerance_class, tolerance_factor, tolerance_applied, tolerance_basis}}. Errors on a material with no usable density/price and no override, an unknown process/tolerance class, or a non-positive volume/quantity/machine time.

tolerance_cost_checkA

Price a tolerance scheme against the process that has to hold it — the missing link between tolerance_stackup ("what tolerance works") and cost_estimate ("what does it cost").

Per toleranced dimension: its ISO 286 IT grade as a FLOAT (a band between IT6 and IT7 reports 6.4, not 7), the cheapest machining operation that holds that grade naturally (drilling ~IT11, milling ~IT10, turning ~IT9, reaming ~IT7, grinding ~IT6), a relative cost index normalised to 1.0 at process's natural capability, and a verdict: 'ok', 'in_process_tightening' (tighter but reachable in the same operation), or 'needs_secondary_operation' — the flag, meaning the part silently acquired an operation nobody costed. pass is false when any link flags. Cost roughly doubles every 1.5 IT grades tightened below natural capability; above it only inspection/scrap falls. total_cost_index is the sum, so two tolerance SCHEMES over the same chain compare directly.

chain is the same [{name, nominal, plus, minus | tol}] tolerance_stackup takes; instead pass a live handle (+ axis, default_tol, general) and the chain is derived off the solid the same way. process: cnc | injection | casting | sheet | fdm | drilling | milling | turning | boring | reaming | grinding | honing | lapping.

fidelity='correlation', band_pct=50 — the RATIOS are defensible, the absolute index is dimensionless and is not money. Returns {process, links:[{name, nominal_mm, band_mm, it_grade, cost_index, verdict, cheapest_operation, natural_it, note}], n_links, total_cost_index, mean_cost_index, flagged, pass, fidelity, band_pct, basis, escalate_to}.

suggest_looseningA

The loosest tolerance that works: which links can give up tolerance for the biggest cost saving while the stack still passes.

Greedy, one IT grade at a time — every trial loosening is re-verified against tolerance_stackup's seeded Monte-Carlo cpk before it is committed, so nothing it suggests can fail the spec. Candidates are ranked by the tolerance_cost_check curve, so the tightest (most expensive) links get opened first. Loosening preserves each link's MEAN, so the stack's nominal does not move.

Two honest refusals: a chain that does not already meet target_cpk returns ok=False (there is no margin to give away — tighten or re-spec, do not loosen), and an already-loosest chain returns steps=[] with saving=0 rather than inventing a saving. stopped says which: 'no_further_move' | 'max_steps' (re-run on the returned chain to continue) | 'no_margin'.

chain / handle / axis / default_tol / general as in tolerance_cost_check; spec_min/spec_max default to the chain's own worst-case bounds. Returns {ok, steps:[{link, index, from_it, to_it, from_band_mm, to_band_mm, cost_before, cost_after, saving, cpk}], stopped, chain (the loosened scheme), cost_index_before, cost_index_after, saving, saving_pct, cpk_before, cpk_after, target_cpk, spec, note, fidelity, band_pct, basis}.

cnc_machinability_checkA

3-axis CNC machinability screen off a live solid — pure geometry, NO CAM engine (no toolpath, no gouge check, no holder collision).

Counts SETUPS from a tool-approach census: for every machined face, which of ±X/±Y/±Z can both address it (the normal does not point away — a wall parallel to the tool axis is milled by the cutter's periphery) and reach it (a ray from the face escapes the solid, the same caster the moldability undercut check uses), reduced to a minimum cover. Faces lying on the stock envelope are excluded: they are billet surfaces, and counting them would quote six setups for a plain block.

Findings: 'undercut' (a machined face no principal approach reaches — 5-axis, a special cutter, or a redesign), 'deep_pocket' (depth/(2·corner radius) past max_l_over_d — the corner radius caps the cutter and it cannot reach), 'small_radius' (an internal corner below the smallest cutter quoted, including a SHARP planar corner reported as radius 0, which no rotating tool can produce), 'thin_wall'. pass is false when any fires; more than max_setups is a warning, not a failure. fidelity='correlation', band_pct=None (an ordinal screen — rank variants with score, don't gate on it).

Returns {setups, setup_directions, coverage, machined_faces, stock_faces, machined_area_mm2, min_internal_radius_mm, max_l_over_d_seen, undercut_faces, findings:[{code, severity, feature, detail}], warnings, score, pass, fidelity, band_pct, basis, escalate_to='cnc_time_estimate', limitations, n_faces}.

cnc_time_estimateA

Machining time for a live solid from a material-removal-rate model — the honest cnc machine time cost_estimate's flat volume table cannot give.

stock         = bbox grown by stock_allowance_mm per side
roughing_min  = (stock − part volume) / MRR(material)
finishing_min = machined face area / finish area-rate(material)
total         = (roughing + finishing)/utilisation · tolerance factor
                + setups · setup_min

MRR is per material class (aluminium 60, steel 12, stainless 6, titanium 2.5 cm³/min …) — ratios that track the standard machinability ratings amplified by the depth-of-cut headroom a soft alloy allows on the same spindle. Stock-envelope faces are excluded from the finishing area: facing a billet is not finishing a pocket. setups defaults to the count cnc_machinability_check derives from the same solid, so the two tiers agree. tolerance_class ('IT7', 7, …) scales the cutting time through the shared tolerance-cost corpus, so a tolerance costs the same here as in tolerance_cost_check.

fidelity='correlation', band_pct=50 — the RSS of MRR scatter (±40 %) and cut-time utilisation scatter (±25 %), against the flat table's ±100 %; supply a measured mrr_cm3_min and it tightens to 30. Feed machine_time_hr into cost_estimate(machine_time_hr=…) to replace the table there too.

Returns {machine_time_min, machine_time_hr, roughing_min, finishing_min, cutting_min, setup_min_total, removed_volume_mm3, stock_volume_mm3, removal_fraction, machined_area_mm2, setups, setups_basis, material_class, material_basis, mrr_cm3_min, finish_cm2_min, utilisation, tolerance, bbox_mm, part_volume_mm3, fidelity, band_pct, basis, warnings, next}.

slice_estimateA

First-order FDM slice estimate (analytic, NO slicer needed; see slice_gcode_submit for the real PrusaSlicer CLI). mass_g = volume·density (Materials DB); deposited = volume·(wall_fraction + infill·(1−wall_fraction)) so at 100% infill filament_g == mass_g; layer_count = ceil(bbox height/layer_height); print_time from nozzle volumetric flow.

material may be any Materials-DB card name (material_list / material_get), or anything at all if you supply density_g_cc yourself — it overrides the DB lookup. A generic word like 'polymer' is a CATEGORY, not a card, and 'nylon' is a near-miss for one, so neither carries a density; the error names both exits. filament_dia_mm (1.75 default, 2.85 for the older standard) sets the spool stock the filament length is computed against.

Returns {mass_g, filament_g, deposited_volume_mm3, layer_count, print_time_min, infill_fraction}. Errors on a material with no density and no override, a negative volume, a bbox shorter than [x,y,z], or a non-positive layer height.

slice_gcode_submitA

Slice a real part with the PrusaSlicer CLI, asynchronous — the external-CLI upgrade of the analytic slice_estimate: real perimeters, infill patterns, supports, travel/acceleration, and the slicer's own print-time model. Requires a PrusaSlicer install ('apt install prusa-slicer' / the AppImage); when absent this returns {ok:false, reason, install} rather than raising.

Pass a body handle (exported to STL in the modeller) or a prepared stl_path. infill_fraction is 0..1 (full infill auto-switches the fill pattern — PrusaSlicer's default refuses 100%); material/density_g_cc set the filament density used to turn the sliced volume into grams. With a body the result also carries the analytic estimate and the deposited_ratio between them (a 20 mm cube at 100% lands ~1.008 — the skirt).

Returns the degradation dict or {job_id, status, cache_hit}; poll job_result for {ok, gcode_path, filament_mm, filament_cm3, filament_g, print_time_s, print_time_text, layer_count, config (the slicer's echoed settings), analytic?, deposited_ratio?}.

mechanism_kinematicsA

Closed-form planar mechanism kinematics — exact, NO external solver (the static pre-check / gate for mechanism_simulate_submit). Pick mechanism:

  • 'fourbar': link lengths crank/coupler/rocker/ground -> {mobility_dof (=1), grashof: {condition, type, input_crank_fully_rotates, shortest}, reachable, n_reached, coupler_path [[x,y]...], reachable_bbox_mm}. config 'open'|'crossed'.

  • 'slider_crank': crank_mm/conrod_mm (+ wrist_offset_mm) -> {stroke_mm (exactly 2·R in-line, independent of conrod), x_tdc_mm, x_bdc_mm, inline_stroke_exact}.

  • 'gruebler': n_links (incl. ground) + joints ([{type}...]) -> {mobility_dof}.

Returns the per-mechanism dict above. Raises on an unknown mechanism or a link set that cannot close.

mechanism_simulate_submitA

Simulate a rigid-link mechanism's DYNAMICS with PyBullet, asynchronously (the MBD family; requires the mbd extra — pip install 'ankusdrive[mbd]'). Use mechanism_kinematics first for the exact closed-form gates (DOF, Grashof, stroke).

links is a tree: [{name, box_mm:[lx,ly,lz], mass_g, parent (link index, −1 = fixed base), joint_type ('revolute'|'prismatic'|'fixed'), joint_axis:[x,y,z], joint_at_mm:[x,y,z] (in the parent frame), com_mm:[x,y,z]}]. drivers: [{link, rate_dps}] (revolute) or [{link, rate_mm_s}] (prismatic). Optional obstacles ([{box_mm, at_mm}]) for through-motion contact, base, gravity (m/s², default [0,0,−9.81]), dt_s, duration_s. gears ([{link_a, link_b, ratio, axis?, max_force?}]) couples two revolute links by ω_b = −ω_a/ratio (ratio = Nb/Na for an Na/Nb external mesh) — the moving image of the gear-train ratio gate.

Returns immediately. If PyBullet is absent: {ok:false, reason, install, mobility_dof, n_links}. Otherwise {job_id, status, cache_hit, mobility_dof}; poll job_result(job_id) for {trajectories, orientations (per-link world quaternion, sampled with trajectories), max_torques, collisions_through_motion (with the sim time of each contact), reachable_envelope {bbox_mm}, mobility_dof}.

topology_optimize_submitA

Minimum-compliance topology optimization (in-house SIMP; NO external solver), asynchronous because each iteration solves an FE system. Optimizes a 2-D rectangular design domain (nelx×nely unit cells) — or, when nelz is set, a 3-D nelx×nely×nelz grid of trilinear hexahedra — to the stiffest layout that holds Σdensity = keep_fraction (the Optimality-Criteria update holds it exactly); penal is the SIMP penalty (≈3), rmin the cone filter radius. Default BCs (both): the whole left face clamped + a unit downward load at the right-face centre. 2-D overrides: fixed_dofs / load=[dof_index, value]. 3-D overrides: loads=[[i,j,k,axis,value],...] point loads at node grid coords (axis 'x'|'y'|'z'), fixed_nodes=[[i,j,k],...] clamped nodes, and keep_out/keep_in lists of half-open element-index boxes [i0,i1,j0,j1,k0,k1] forced void / forced solid (keep-out regions and must-keep pads).

Returns immediately {job_id, status, cache_hit}; poll job_result for {density (2-D: nely×nelx grid; 3-D: nelz×nely×nelx voxel field, density[k][j][i] with j=0 at the bottom — this IS geometry), mass_fraction (==keep_fraction), compliance, compliance_initial, iterations, converged, gray_fraction, solver (3-D: which linear-solve backend ran)}. Threshold + voxel→solid back in the modeller with topology_to_solid, then gate with mass_properties (mass ≤ keep_fraction·original) and interference_check vs keep-outs.

topology_to_solidA

Reconstruct a FreeCAD solid from a topology-optimization density field — the modeller-side close of the loop opened by topology_optimize_submit, whose density this consumes. 2-D (nely×nelx grid): thresholds (a cell is solid when density ≥ threshold), run-length-merges each row into solid spans, tiles each span as a cell_mm box extruded thickness_mm in Z (row 0 at the top). 3-D (a nelz×nely×nelx voxel field from the nelz mode): greedy-merges solid voxels into maximal boxes at (i·cx, j·cy, k·cz) — j=0 at the bottom, thickness_mm ignored. Fuses into one static Part::Feature. cell_mm is a scalar or [cx, cy(, cz)] mm; thickness_mm defaults to the smaller cell edge; placement is an optional [x, y, z] mm origin offset; name names the object. Runs synchronously (it builds geometry — no jobs.py poll).

Returns {handle, name, volume (mm³), solid_cells, total_cells, mass_fraction (== solid_cells/total_cells — must be ≤ keep_fraction within one cell), n_solids (disjoint bodies; >1 means a split load path), threshold, nelx, nely, nelz (None for 2-D), bbox_mm}. Gate it with mass_properties (mass ≤ keep_fraction·original) and interference_check against keep-out regions, per SIMULATION_EXAMPLES §5.

async_demo_submitA

Reference async long-solve: launch a job that runs OFF the MCP channel and return immediately, so a multi-minute solve never blocks the worker. (This demo just computes for duration_s then returns a deterministic result; a real FEM/CFD solve plugs into the same facility — see ankusdrive/jobs.py.) Returns {job_id, status, cache_hit}; poll with job_status / job_result. A re-submit with identical (duration_s, value) is a content-hash cache hit (no recompute).

job_statusA

Lightweight poll of any async job (from an *_submit tool). Returns {job_id, kind, status: 'running'|'done'|'failed', elapsed_s, meta} (+ error when failed) WITHOUT the result payload — cheap to call in a loop.

job_resultA

Fetch an async job's outcome. Returns {job_id, kind, status, elapsed_s, result (when done) | error (when failed)}; while running neither is set. discard=True frees a terminal job (and its cache entry) once you have it.

job_listA

List every async job this worker session. Returns {count, jobs:[{job_id, kind, status, elapsed_s}]} in submit order, plus main_thread_queue:{queued, ran, failed, drains, pending} — the diagnostic for a shape optimization that looks stuck. Those builds run on the worker's main thread, and that queue is drained once per request: pending high with drains climbing means the work is slow; drains flat means nothing is polling, so nothing is advancing.

recipe_listA

List every registered part recipe — named, parameterized, declared-input build templates (issue #136). Returns {schema, count, recipes} where each recipe maps to {doc, required, optional, emits}; the cheap directory to browse before picking and parameterizing a recipe with recipe_schema / recipe.

recipe_schemaA

Return one recipe's declared INPUT SCHEMA — its driving parameters with type/unit/default/range. Returns {schema, recipe, doc, inputs:[{name, type, unit?, default?, min?, max?, required, choices?, doc?}], emits}. recipe names a registered recipe; an unknown name fails loudly. This is the contract a parametric regeneration is authored against.

recipe_validateA

Validate a recipe reference {recipe, inputs} WITHOUT building it — the cheap front door (mirrors validate_manifest). Catches an unknown recipe, a missing required input, and a wrong-typed / out-of-range / bad-unit / unknown input. Returns {ok, problems} — ok is True iff problems is empty. Run before building or merging to reject a malformed parameterization before geometry is spent.

recipeA

Build a registered part recipe into the active document (issue #136): "regenerate with new parameters" = "re-run the recipe." Validates {recipe, inputs} against the declared schema (typed units + ranges) at the door, then runs the deterministic build — geometry + publish_interface + declare_intent. Returns {recipe, schema, inputs, handle, name, interfaces, intent, part}.

items_validateA

Validate an items.json registry (the PLM item/document/file split, #140 C1). Checks the schema stamp ("ankusdrive.items/1"), each item record's shape, that part numbers are unique, and the held reserved rev/lifecycle fields. An item is the logical part (part_number + rev + lifecycle + metadata), distinct from its file artifact(s); part numbers are non-significant + sequential, with meaning in queryable metadata.

registry: path to the items.json sidecar.

Returns {ok, problems, schema, count} — ok is True iff problems is empty.

items_resolveA

Resolve an item-reference (an item id) to its artifact file(s) against an items.json registry — identity, not a bare path, so renaming/moving a file updates the item's files[] without breaking references. Returns {ok, files}, or {ok:false, problems} on a dangling reference (an unknown item id).

items_newB

Allocate a non-significant sequential part number and register a new item in an items.json sidecar (created if absent), writing it back. The reserved rev / lifecycle fields are seeded with held defaults (the #141 state machine, not C1).

registry: path to the items.json sidecar (created if it does not exist). item: the new item's stable logical id (what manifests reference). files: optional list of artifact paths the item maps to. metadata: optional free-form, queryable attributes (where "meaning" lives).

Returns {part_number, item, registry}.

items_check_manifestA

Reference-integrity guard (#140 C1): check every item-reference in a manifest ({"item":""} on a component/instance) resolves against an items.json registry. Returns {ok, problems} — a dangling item-ref is caught before a merge, removing the "a part is its filename" fragility.

get_interfaceA

Read back a single published interface FRAME by name from a component (issue #139). The reference-by-name primitive feature templates ride on: an unpublished name fails loudly. Returns {handle, name, frame}.

feature_listA

List every registered FEATURE TEMPLATE — the PowerCopy/UDF analog of a part recipe: a reusable feature with declared reference-geometry inputs (a frame, an f_/e_ tag, an axis) plus scalar parameters, stamped onto a host by name (issue #139). Returns {schema, count, templates} where each maps to {doc, refs, required, optional, emits}; the directory to browse before picking one with feature_schema / feature_instantiate.

feature_schemaB

Return one feature template's declared REF+INPUT SCHEMA — its reference geometry (name/kind) and its scalar parameters (type/unit/default/range). Returns {schema, template, doc, refs:[{name, kind, required, doc?}], inputs:[{name, type, unit?, default?, min?, max?, required, choices?, doc?}], emits}. An unknown name fails loudly.

feature_validateA

Validate a feature instantiation {template, refs, inputs} WITHOUT building it — the cheap structural front door (mirrors recipe_validate). Catches an unknown template, an unknown/missing required reference, a malformed reference value, and every scalar-input failure (missing required, out of range, wrong type, bad unit, unknown key). A tag that doesn't resolve against real geometry is caught at feature_instantiate time. Returns {ok, problems}.

feature_instantiateA

Stamp a registered FEATURE TEMPLATE onto a host body at reference geometry supplied BY NAME (PowerCopy/UDF, issue #139) — e.g. a mounting_boss onto a published seat frame, or a bolt_pattern onto an f_ face tag. Validates {refs, inputs} at the door, resolves each reference against the host's CURRENT geometry (an f_ tag / interface name that doesn't resolve fails loudly), then runs the deterministic build — geometry + publish_interface + declare_intent.

template: registered template name (feature_list to browse). host: the handle of the body to stamp onto. refs: reference inputs by name — an interface name, an f_/e_ tag, or a literal {origin, z_axis?, x_axis?} frame, per the template's declared ref kinds. inputs: scalar parameters (typed + unit-bearing + range-checked).

Returns {template, schema, host, refs, inputs, handle, name, interfaces, intent}.

family_validateA

Validate a variant-family design table (issue #138, B1) — a row x column table where row = a variant (keyed by a size designator) and column = a recipe parameter / feature-flag / material. Loads CSV or JSON and checks the recipe, mode, key column, duplicate/missing size keys, and every per-row recipe-door value; each problem names the row+column. Returns {ok, problems}.

table: path to the family table (.csv or .json).

family_materializeA

Materialize a whole variant family from ONE design table (issue #138, B1) — "make all the gears" becomes a table, not a loop. For each row, in table order, builds the part with its recipe (A1, #136) and allocates one item + one sequential part number (C1, #140). Subsumes standard-part catalogs: a bearing catalog is a family table keyed by designation, sourced from the ISO corpus.

Two modes (both supported): "instances" (each variant its own released file + part number) and "configurations" (variants share one artifact). Builds into the active document. Returns {schema, family, recipe, mode, key, count, rows, registry}.

table: path to the family table (.csv or .json). registry: optional items.json path (created if absent, written back). mode: optional override of the table's mode (instances|configurations).

substitutability_checkA

Liskov-substitutability gate — Form/Fit/Function as code (§7.1, #147). Take an assembly that gates green with variant A in a slot, swap in variant B (a different family row, or any part claiming the same interface), and re-run merge_assembly + all the gates. Still green ⇒ B is interchangeable with A — by construction a compatible (MINOR/PATCH) change ⇒ revise the existing part number; a gate now fails ⇒ the swap broke Form/Fit/Function ⇒ a new part number. Purely deterministic (no API, no judgment); the substitutability test #138 (B1 families) and #146 (the interface registry) call.

manifest: path to a manifest that gates green with variant A in slot. slot: the component id to swap (variant A → variant B). variant: the replacement component spec — a dict with exactly one of file/manifest/library (the same one-source rule merge_assembly enforces). verify_baseline: re-merge the base assembly first and require it green so the premise is honest (default True).

Function, not just Form and Fit (#261): if the swapped-in variant declares a PERFORMANCE contract (#226), it is part of the comparison. A contract measured as NOT met breaks the performance gate like any other. A contract with NO recorded verdict yields a THIRD answer — substitutable: null, verdict 'performance_unproven' — because an unverified spec is not a passed spec, and handing an unproven part an existing part number is the silent pass #226 prevents.

Returns {schema, slot, variant, baseline_ok, swap_ok, substitutable (True | False | null), verdict ('substitutable' | 'not_substitutable' | 'baseline_not_green' | 'performance_unproven'), broken_gates (the NAMED gate(s) the swap broke), broken (gate→violations), classification (compatibility/semver/decision), performance? (only when a component declares a contract), reports}.

lifecycle_editableA

The cheap "is this editable?" check a builder runs before writing (#141 C2). An item is editable only in lifecycle state in_work; in_review, released and obsolete are frozen (released = immutable, the API-stability guarantee).

registry: path to the items.json sidecar. item: the item id to check.

Returns {ok, editable, state}.

lifecycle_transitionA

Move an item through the lifecycle state machine in_work -> in_review -> released -> obsolete, guarded by a transition table (#141 C2). An illegal edge (e.g. skipping review, or re-opening a released item in place) is rejected loudly; releasing stamps the item's first revision and freezes it.

registry: path to the items.json sidecar (written back on success). item: the item id to transition. to: the target lifecycle state. actor / note: optional provenance recorded in the item's transition log.

Returns {ok, state, rev}, or {ok:false, problems} on an illegal transition.

lifecycle_classify_changeA

The deterministic Form/Fit/Function predicate (#141 C2): compare an item's before/after attributes and decide rename-vs-revise. A change touching a Form/Fit/Function (public, interface-defining) attribute breaks interchangeability => "new_part_number" (allocate a new number); a change to only internal/hidden attributes is interchangeable => "revise" (bump the revision, same part number).

before / after: attribute objects (the item's interface-defining + internal attributes, before and after the proposed change). extra_f3: optional map of extra attribute name -> F3 leg ("form"/"fit"/ "function") for domain-specific interface attributes.

Returns the verdict {disposition, f3, changed, f3_changed, categories, reason} where disposition is "revise" / "new_part_number" / "noop".

lifecycle_apply_changeA

Apply a change to a RELEASED item, dispatching on the F3 predicate (#141 C2) — the sanctioned way to change a frozen part. An F3-preserving change opens a new revision on the SAME part number (rev A->B, back to in_work); an F3-breaking change allocates a NEW part number as a new item (new_item required) carrying a supersedes back-link, leaving the released item untouched.

registry: path to the items.json sidecar (written back on success). item: the released item id being changed. after: the proposed new metadata (attribute object). new_item: id for the new item when the change is F3-breaking. extra_f3 / actor / note: optional, as in lifecycle_classify_change / lifecycle_transition.

Returns {ok, disposition, item, part_number, rev, ...}, or {ok:false, problems} (e.g. the item is not released, or an F3-break lacks new_item).

scaffold_projectA

Lay out a well-formed project in one call (issue #143 / D1) — promote the MULTI_AGENT.md §3/§7 directory convention to a primitive. Creates the convention directories (components/, .dp_lib/), an item registry (items.json, #140), a seed assembly manifest from the supplied components/instances, and the project.json container that ties them together. The result loads clean and merge_assembly consumes it unchanged once its components resolve.

base_dir: the project root directory (created if absent). name: the project id (naming-convention checked). components/instances/shared_parameters: the assembly manifest content. master: optional component id to record as the master/skeleton single-source-of- truth slot (the lean interface-geometry skeleton children mate against). items: optional {item_id: {files?, metadata?}} to seed the item registry with.

Returns the layout {project_file, manifest, registry, components_dir, lib_dir, lockfile, master, dirs}.

project_validateA

Validate a project.json container (issue #143 / D1): the schema stamp ("ankusdrive.project/1"), the naming convention on the project name, the conventional path fields, and — relative to the project directory — that the referenced manifest + item registry exist, the components directory is present, and a named master/skeleton is a real component of the assembly manifest.

project: path to the project.json container.

Returns {ok, problems, schema, name} — ok is True iff problems is empty.

project_check_referencesA

Reference-integrity guard (issue #143 / D1) — catch a broken cross-file reference before a merge (the chronic PDM failure mode). Loads a project's assembly manifest + item registry and checks every reference resolves: a moved / renamed / missing component file, a dangling item-ref (an unknown id, or a resolved file missing on disk), an instance naming an unknown component, and naming-convention violations.

project: path to the project.json container.

Returns {ok, problems} — ok is True iff every reference is live, so a broken reference is reported here instead of as a cryptic merge failure.

project_resolve_manifestA

Resolve a project's item-ref components to file components and write a merge- ready manifest (issue #143 / D1, the deferred #140 seam). Each component naming an item (items.json identity, not a bare path) is lowered to a {file} component with the CAD artifact resolved from the registry, so merge_assembly consumes the result unchanged — renaming/moving a file updates the item's files[] in one place without breaking any reference.

project: path to the project.json container. out: output manifest path (defaults to .resolved.json next to it).

Returns {path, lowered} — the written path and the lowered manifest object; a dangling item-ref fails loudly.

where_usedA

Where-used / impact analysis (issue #142, C3; MULTI_AGENT.md §9): traverse the lockfile depends_on graph and report every parent that CONSUMES an item — the blast radius, so exactly those parents re-dispatch when the item changes. The lockfile records edges consumer -> consumed (lid depends_on housing); where-used is the reverse reachability of the item (everything that reaches it).

lockfile: path to the lockfile (the JSON assembly_lock wrote). item: the item / component id to query. direct: if true, also surface only the immediate mates separately.

Returns {item, where_used (the full transitive blast radius), direct (the §9 immediate-neighbour layer)} — an unknown item fails loudly, never a silent empty set.

change_impactA

The where-used impact report for a set of changed items over a lockfile graph (issue #142, C3) — surfaces the §9 stale set as an item-level impact report BEFORE a change is committed.

lockfile: path to the lockfile (the §9 dependency graph). changed: the list of changed item / component ids (an ECO's affected set).

Returns {changed, stale (the §9 immediate re-dispatch consumers), where_used (the full transitive blast radius), ok (true iff nothing is impacted)}.

eco_validateA

Validate an ECO (engineering change order) record (issue #142, C3) — the cheap front door. Checks the schema stamp, a non-empty id + affected item set, a present disposition, and an effectivity carrying exactly one of date|serial|revision.

eco: the ECO object.

Returns {ok, problems} — ok is True iff problems is empty.

eco_createA

Build an ECO change-order record (issue #142, C3) — turn a change into a record, not a silent mutation (the diff IS the change order, mapping onto a git commit/PR). Optionally compute its where-used impact over a lockfile in the same call, so the blast radius travels with the record.

id: the ECO id (e.g. "ECO-0001"). affected: the list of changed item / component ids. disposition: the change disposition (use_as_is | rework | scrap | revise | ...). effectivity: a dict with exactly one of date|serial|revision (when it takes effect). title / note: optional human description recorded on the record. interface_change: true if a published interface moved (the §9 stale trigger). lockfile: optional path — when given, the result embeds an impact report. out: optional path — write the git-diffable ECO sidecar there.

Returns the ECO object (with impact when a lockfile is supplied); a malformed ECO fails loudly.

baseline_createA

Pin a labeled, immutable BASELINE (issue #142, C3) — a {item: revision + content fingerprint} snapshot over an items.json registry (a git-tag / lockfile over the item graph) for reproducible rebuilds. The fingerprint pins the artifact BYTES, so a rebuild is verifiable byte-for-byte.

label: the baseline label (e.g. "v1.0"). registry: path to the items.json sidecar. items: optional subset of item ids to pin (default: every item). base_dir: artifact root for fingerprinting (defaults to the registry's directory). note: optional description. out: optional path — write the git-diffable baseline sidecar there.

Returns the baseline object. Deterministic: same state -> identical bytes.

baseline_verifyA

Verify a rebuild against a pinned baseline (issue #142, C3) — the reproducible- rebuild gate. Re-resolves every pinned item from the current registry + artifacts and checks it still matches the pinned rev AND content fingerprint; a drifted input (changed bytes, a bumped rev, a vanished item) is caught, never silently accepted.

baseline: path to the baseline sidecar. registry: path to the current items.json sidecar. base_dir: artifact root for fingerprinting (defaults to the registry's directory).

Returns {ok, label, drifted (item/field/expected/actual), missing} — ok is True iff the rebuild reproduces the baseline exactly.

release_packageA

Produce the vendor/RFQ deliverable bundle for one item at one revision — STEP + drawings + BOM + inspection package + a checksummed manifest — gated by the item's lifecycle state and stamped with its ECO.

Every piece of this exists as its own tool. What this adds is the guarantee that ties them together: the STEP, the PDF, the BOM and the title block all describe the SAME revision of the SAME item. That is the whole point of a release, and it is enforced BEFORE a single file is written:

  • the item must be in a releasable lifecycle state (released), or draft must be set — which watermarks EVERY artifact PRELIMINARY (burned into the drawing, and carried as a format-legal comment in the STEP header, the DXF and the CSVs). An obsolete item is refused in both modes.

  • drawing_gate must pass for every included page — with require_ballooned when the inspection kind is requested.

  • the title block's part number / revision / material must MATCH the items registry. A mismatch is a FAILURE carrying expected-vs-actual, never a silent fix: quietly rewriting the print would destroy the only independent check that it and the model describe the same thing.

A refused release writes NOTHING — no half-populated directory a build script could mistake for a package.

registry / item: the items.json sidecar and the item id being released. out_dir: where the bundle is written (created if absent). kinds: any of "step", "drawing_pdf", "drawing_svg", "drawing_dxf", "bom_csv", "inspection", "manifest_json". Default step/drawing_pdf/drawing_dxf/ bom_csv/manifest_json. manifest_json is always added; inspection (ballooned print + plan + blank AS9102 form) implies a drawing kind. draft: cut a PRELIMINARY package from an unreleased item. rfq: the quote flavour — adds quantity breaks and the cost_estimate rollup as a quote-COMPARISON baseline (fidelity "correlation", band_pct 100 — trust the ratios, not the dollars) and drops internal-only artifacts (the inspection package, which states your acceptance criteria). eco: the change order this release is cut under; defaults to the item's metadata.eco. Stamped into the manifest AND the title block's REV cell. pages: page handles/names/labels (default: every page in the document). handle / object: the geometry to export (default: the first page's main view source — literally the solid the drawing dimensions). process: drawing_gate process ("auto" | "prismatic" | "turned"). density / recursive: BOM options, as in bom_extract. quantity_breaks / cost_process / material: RFQ pricing inputs (default [1, 10, 100], "cnc", and the item's declared material).

Determinism: the same item at the same revision produces a BYTE-IDENTICAL package. The exporters' wall-clock header stamps (the STEP FILE_NAME timestamp above all) are scrubbed to a fixed epoch, so a re-released package is diffable by checksum.

Returns {ok, dir, item, part_number, rev, lifecycle, eco, material, draft, watermark, flavor, kinds, dropped_kinds, implied_kinds, pages, files:[{name, kind, bytes, blake2b}], manifest, manifest_path, verify, problems}. ok=False means the gate refused and nothing was written; each problem carries {code, where, field, expected, actual, reason}.

sheet_baseA

Start a sheet-metal part: the base flange, a closed straight-sided profile extruded to thickness_mm. Everything else (flanges, tabs, hems, the flat pattern, the DXF) hangs off the handle this returns.

profile: [[x, y], ...] in the XY plane, implicitly closed. sketch: alternatively a handle to a closed, planar, straight-sided sketch — on any plane. Arcs are REJECTED rather than silently faceted, because a faceted flat pattern is a wrong flat pattern. material: any Materials-DB name (e.g. "Steel-A36", "AL6061-T6", "SS304"); it selects the K-factor and minimum-bend-radius corpus rows.

IMPORTANT — the profile is the flat face TANGENT TO TANGENT, not the outside dimension. Bends grow OUTWARD from the profile boundary, exactly as a base flange behaves in any sheet-metal CAD, so a U-channel of 100 mm outside width with R = t = 2 starts from a 92 mm profile.

The bend model lives alongside the handle for the life of the worker session, like every other handle: unfolding is a property of the FEATURE TREE, not of the fused solid, so a part reopened from disk in a new session is a solid rather than a sheet part. Cutting one (boolean_op cut, e.g. to drill it) keeps it a sheet part; fusing arbitrary material onto it does not.

Returns {handle, name, volume, thickness_mm, material, profile, area_mm2}.

sheet_flangeA

Bend a flange off a free edge of a sheet part.

edge: a stable e_* tag from list_edges (or 'EdgeN'/int) on a STRAIGHT free edge of a flat region — a bend line is the intersection of two planes, so an arc cannot carry one and is rejected. angle_deg: the bend angle, i.e. the deviation from flat, so 90 is a right-angle flange. Must be in (0, 180]. inner_radius_mm: inside bend radius; defaults to the material thickness. direction: 'up' (toward the region's outward normal) or 'down'. length_from: what length_mm measures — the number most often misread on a sheet drawing. 'outer' (default) to the outside virtual apex, which is what a drawing dimension normally means; 'inner' to the inside apex; 'tangent' for the straight leg past the end of the bend. width_mm / offset_mm: narrow the flange to part of the picked edge. k_factor: pins K for THIS bend only. Leave it unset and the choice defers to sheet_unfold — the folded solid does not depend on K at all, only the flat pattern does.

Consumes the input handle (it is hidden, having become part of the result).

Returns {handle, name, feature, kind, volume, angle_deg, inner_radius_mm, leg_tangent_mm, length_from, direction, span_mm, thickness_mm}.

sheet_tabA

Extend a sheet part with a coplanar tab — a flat ear off a free edge with no bend at all (a mounting lug, a weld tab, a snap-off).

Mechanically it is a zero-angle flange and shares that code path exactly: length_mm is how far the tab reaches past the edge, width_mm/offset_mm place it along the edge (default: the whole edge). It adds no bend to the bend report and no bend line to the DXF, but it does grow the flat pattern.

Returns the same dict sheet_flange does (angle_deg 0, inner_radius_mm 0).

sheet_hemA

Fold a hem back on itself — the 180-degree return that stiffens a free edge and buries the sharp cut line so the part is safe to handle.

kind: 'closed' (inside radius t/2, gap t) or 'open' (radius t, gap 2t). radius_mm / gap_mm: override the style directly; the gap between the returned leg and the parent is exactly 2R, so gap wins as radius = gap/2.

length_mm is ALWAYS the return leg measured from the end of the bend: a 180-degree bend has no virtual apex to dimension to — the outside surfaces are parallel and never meet — so an 'outer' dimension would be infinite. For the same reason a hem reports a bend allowance but no bend deduction, and sheet_check screens it as a two-hit hem (bend, then flatten in a hemming die) exempt from the air-bend radius and flange rules. A teardrop hem wraps past 180 degrees and is out of scope.

Returns the same dict sheet_flange does, plus {hem_kind, gap_mm}.

sheet_unfoldA

Develop a sheet part into its flat pattern — the blank the part is cut from — and report every bend.

The flat pattern is derived from the bend tree, not reverse-engineered out of the fused solid, so it is exact rather than fitted: each bend contributes its bend allowance BA = angle·(R + K·t), the arc length of the neutral fibre.

k_factor: pins K for every bend. bend_table: a shop's own measured rows [{thickness_mm, inner_radius_mm, angle_deg, allowance_mm | deduction_mm}] — a matching row OUTRANKS the chart, because the shop's press is the ground truth for the shop's press. Without either, K comes from a press-brake corpus keyed by material and r/t. WHICHEVER IT IS, IT IS ECHOED BACK per bend as k_factor + k_source: a flat length whose K you cannot see is a number you cannot check.

build: also create the flat blank as a real solid (holes included) at origin in the XY plane, so it can be measured, exported or nested.

Fidelity: 'exact' only when EVERY bend's K was supplied or table-derived — BA given K is pure arithmetic. One corpus-defaulted bend makes the development a 'correlation' with band_pct, and developed_band_mm gives the resulting millimetre spread of the blank across that K band.

Returns {ok, handle?, name?, volume?, outline, holes, bend_lines, bends, regions, flat_size, flat_bbox, flat_area_mm2, blank_area_mm2, blank_volume_mm3, thickness, material, fidelity, band_pct, developed_band_mm, warnings} — regions being each flat region's polygon, which is what lets the whole report be handed straight back to sheet_refold. Each bend row carries angle_deg, direction, inner_radius_mm, leg_tangent_mm, outer_length_mm, bend_allowance_mm, bend_deduction_mm, outside_setback_mm, k_factor, k_source, and the bend_line / tangent_start / tangent_end segments in flat coordinates. ok=False means the blank cannot be cut as drawn — two feature footprints overlap — with warnings naming which.

sheet_refoldA

Fold a flat pattern back up and check it reproduces the part — the other half of the unfold gate.

This does NOT replay the feature model. It reads the flat pattern back: each leg length is measured off the flat outline, walking outward from the bend's attachment past its reported bend allowance to the far edge of that region. So a wrong allowance, angle or bend direction lands the refolded solid somewhere the original is not, and this reports the disagreement instead of hiding it.

handle: a sheet part to unfold and then refold. flat: alternatively a sheet_unfold report, to refold a development produced elsewhere (or a deliberately corrupted one, to prove the check bites). compare: the handle to check against; defaults to handle, and is skipped when only flat is given. volume_tol_pct / bbox_tol_mm: agreement tolerances.

Note the round trip that is meaningful and the one that is not: refold-vs-folded must match, but flat-vs-folded VOLUME must not, and does not. Bending preserves neutral-fibre length, not material volume — a bend sector's true volume is angle·t·(R + t/2)·w while its flat footprint is angle·(R + K·t)·t·w, and those agree only at K = 0.5.

Returns {handle, name, volume_mm3, bbox, bends, compare?} where compare is {handle, matches, volume_mm3, volume_error_pct, bbox_max_error_mm, tolerance}.

sheet_flat_exportA

Write the flat pattern as a LAYERED DXF — the file a laser, punch or press-brake shop actually quotes and cuts from. This is the deliverable the whole sheet-metal family exists to produce.

Three layers, because a flat pattern without them is not a shop deliverable: CUT carries the closed outer profile and every hole; BEND_UP and BEND_DOWN carry one centreline per bend, so the operator reads the fold direction off the print rather than inferring it. DXF R12 ASCII in millimetres, written directly rather than through TechDraw — a flat pattern is not a drawing view and does not want a sheet frame, a scale or a title block around it. path must end in .dxf.

Takes the same k_factor / bend_table arguments as sheet_unfold, since the outline it writes IS the development.

Returns {ok, path, size, layers, entities, flat_size, blank_area_mm2, bends (with bend_allowance_mm, bend_deduction_mm, k_factor and k_source per bend), fidelity, band_pct, warnings}.

sheet_checkA

Press-brake manufacturability screen for a sheet part — four rules, each with the number it came from:

min_bend_radius — an inside radius below the material's minimum (a corpus value per material: 3t for 6061-T6, 1t for A36, 0.5t for annealed 1100) cracks the outer fibre. min_flange_length — an outer leg under 4t + R has no die shoulder to sit on and dives into the vee. hole_to_bend — a hole whose EDGE is nearer the bend tangent than 2t + R draws into an oval. Holes are read off the real solid, not declared. refold_collision — two features that occupy the same space once folded, found by actually intersecting them rather than by a rule.

A hem is screened as a two-hit hem (bend, then flatten) and exempted from the air-bend radius and flange rules, which would otherwise fail every hem ever drawn. A flat pattern whose feature footprints overlap is a finding too, not a warning dropped on the floor. An unrecognised material degrades to a bend-class fallback WITH an info finding saying so, rather than skipping the rule silently.

min_flange_t / hole_to_bend_t: override the thresholds (multiples of thickness).

fidelity='correlation' with band_pct=None — these are press-brake rules of thumb, thresholds for ranking and gating rather than measured predictions.

Returns {ok, findings, fail_count, rules, min_bend_radius_mm, min_bend_radius_source, flat_size, blank_area_mm2, k_factors, thickness_mm, material, fidelity, band_pct}. Each finding carries {code, severity, message} plus the measured value and the limit it missed.

Prompts

Interactive templates invoked by user choice

NameDescription
diagnose_setupDiagnose this AnkusDrive install and guide the user through finishing it.

Resources

Contextual data attached and managed by the client

NameDescription
setup_resourceThe current setup/health report (the human-readable `ankusdrive doctor` checklist): FreeCAD + every solver family with per-item fixes. Regenerated on every read; side-effect-free (no FreeCAD boot).

TDQS

B3.2/5.0

Scored across 281 tools

Disambiguation4/5

Despite the enormous surface, most tools declare a clearly distinct purpose, and descriptions go out of their way to draw boundaries (render_view vs render_photoreal vs render_photoreal_submit vs render_job; screen vs *_submit twin pairs; check vs verify gates). A few clusters genuinely blur—moldability_screen/moldability_check/optics_moldability_check, dfm_check/cnc_machinability_check, and the many *_check/*_validate gates—but the text usually disambiguates them.

Naming Consistency4/5

Names are uniformly snake_case with a predictable verb_noun convention (add_*, make_*, list_*, get_*, check_*, verify_*, declare_*, *_submit, *_results) plus consistent family prefixes (fem_*, cfd_*, em_*, sheet_*, items_*, optics_*). Minor deviations exist—single-word domain verbs (pad, hole, pocket, loft) and a mixed verify_/check_ verb style—but the pattern stays readable.

Tool Count1/5

281 tools is an extreme surface, roughly 5–6x the 50-tool threshold the rubric treats as a total mismatch, and far beyond what any agent can navigate without heavy filtering. Even granting the genuinely broad engineering domain (CAD, FEM, CFD, optics, EM, DEM, molding, sheet metal, PLM), the count is overwhelming and unearns its keep at this scale.

Completeness5/5

The surface is exceptionally complete: full CRUD-style CAD/PartDesign modeling, mesh/FEM solvers across modal, buckling, thermal, nonlinear and contact, CFD, FSI, acoustics, EM, DEM, molding, sheet-metal unfold/refold/DXF, DFM, cost, drawing and manufacturability gates, plus a full PLM layer (items, lifecycle, ECO, baseline, release, project scaffolding). It is hard to identify any obvious missing operation or dead end.

Maintenance

ActivityActive
ResponsivenessResponsive