Skip to main content

CLI API reference

The VirtuCath™ CLI API provides a persistent REPL (Read-Eval-Print Loop) interface for interacting with the application programmatically over standard input/output (stdin/stdout). It supports fully headless operation: machine learning training, dataset generation, and direct interaction via AI agents or external scripts.

The API runs continuously, holding the loaded simulation model and Flowdown engine state in memory between commands. It communicates exclusively using single-line JSON strings.

About the examples on this page. Response samples use ... as a visual placeholder for truncated content (omitted object members, additional array entries, etc.). The samples are illustrative: copying one directly will not produce valid JSON. For the canonical schema, call export_schema at runtime; for actual live responses, call the relevant command and read what comes back.

For AI agents: start here

If you're an LLM-driven agent encountering VirtuCath for the first time:

  1. Start the process with ./VirtuCath.exe --cli-api (see Starting the API for the executable path on Windows).
  2. Send {"command": "export_schema"} over stdin. The response contains the full JSON Schema for every command: parameter names, types, required fields, enums. Treat that schema as the source of truth. For the catheter config document those commands carry (the .json design format), send {"command": "export_config_schema"} — see export_config_schema; you do not need to reverse-engineer the format from the wizard.
  3. Use {"command": "get_config"} after any load or wizard_step to inspect the active configuration as ground-truth state.
  4. Build catheters either by passing a .json file path / inline dict to load, or by walking the wizard_step state machine. The wizard validates each layer against the global OD constraint at submission time, so it's the easier path for trial-and-error optimization. Multi-lumen extruded cores are authored the same way (submit_core_profile); see Multi-lumen extruded core.
  5. Wrap each request in a try-step-observe loop: any command that errors returns {"status": "error", "error_code": "<CODE>", ...} for deterministic handling. If you receive RATE_LIMITED, back off for the returned retry_after_s seconds and resend; see Rate limiting.
  6. Running a parametric sweep (varying the construction and reading export_rod_spec back each iteration)? Start with --gym-mode to lift the rate limit, and check the gym_mode block in get_license_info before committing to a long run; see Gym mode. A TRIAL_QUOTA_EXCEEDED response means the session is on a trial license and has spent today's evaluation allowance: backing off will not clear it, only the 00:00 UTC refresh or a paid license. A LICENSE_REQUIRED response means the session has neither a license nor a live trial; export_rod_spec is the only command that requires one.

get_static_kpis and get_dynamic_kpis accept an optional keys array to trim the response to named KPIs. No other command does.

Command index

CategoryCommands
Introspectionexport_schema · export_config_schema · get_config · get_license_info · get_materials
Configurationload · validate_config · wizard_step · add_temporary_material · create_blend
Static analysisget_static_kpis · calculate_flowdown · run_kink_simulation · run_push_simulation · braid_transfer
Dynamic simulationset_environment · reset_simulation · set_proximal_fixture · step_simulation · set_pullwires · get_dynamic_kpis
Outputrender_simulation · generate_graphics · generate_reports · export_rod_spec
DOE / Design Sweeprun_doe · get_doe_results
Lifecycleexit
Retiredcalculate_kink_safety: returns COMMAND_RETIRED; use run_kink_simulation

JSON config glossary

The same catheter .json schema is used by load, validate_config, and the Setup Wizard GUI. Call export_config_schema for the full machine-readable definition (the exact schema the validator runs, plus the loader rules that live outside it); the most-referenced fields are:

FieldWhereMeaning
catheter_definition.nametop levelHuman-readable design name (used in reports).
catheter_definition.dimension_modetop levelOne of OD_FIXED, ID_FIXED, or OD_ID_FIXED (default OD_FIXED). Controls how the layer stackup is solved.
unit_systemtop levelDisplay preference only (Metric or Imperial). Every dimensional field in the file is _mm / _m regardless of this value; Imperial only changes how the GUI formats numbers on screen.
catheter_definition.mandrel_diameter_mmtop levelMandrel (innermost) starting diameter, used when dimension_mode = OD_FIXED. Also the fallback for ID_FIXED files that carry no fixed_inner_diameter_mm. In the ID-driven modes it never needs authoring (hydration reads fixed_inner_diameter_mm directly); validate_config advises when a present value disagrees with the fixed ID.
catheter_definition.fixed_inner_diameter_mmtop levelInner diameter target when dimension_mode is ID_FIXED or OD_ID_FIXED. Takes precedence over mandrel_diameter_mm in both.
catheter_definition.overall_diameter_mmtop levelOuter diameter target when dimension_mode is OD_FIXED or OD_ID_FIXED. In ID_FIXED the OD is an outcome of the stack (bore + 2 × layer thicknesses) and is derived when absent; validate_config reports the computed value as an advisory when a declared OD disagrees with it.
catheter_definition.sections[]arrayOrdered list of catheter sections, proximal → distal. Index 0 (Section 1) is the proximal end; the last entry is the distal tip.
sections[].length_mmper sectionSection length; must be greater than 0. The Setup Wizard additionally enforces a 2 mm minimum per section and a 1500 mm total, but load and validate_config do not.
sections[].layers[]per sectionLayer stack, defined inside-out (liner first, jacket last).
sections[].is_manual_stiffnessper sectionWhen true, skips layer-based stiffness calculation and uses user-supplied EI/GJ overrides. Certain geometry-derived KPIs (like linear_density_kg_per_m) become null.
layers[].material_nameper layerMust name a material in the active material library. Use get_materials to list. Case and trademark-symbol aliases are accepted (pebax(r) 7233 sa 01 med finds PEBAX® 7233 SA 01 MED); the canonical library name is written back on load and echoed in the load / validate_config responses. A name that matches nothing hydrates as a generic 100 MPa placeholder, with a warning.
layers[].reinforcement_typeper layerNone, Polymer, Braid, or Coil. Polymer is a passive polymer layer; Braid and Coil additionally require reinforcement_settings. A triaxial braid is a Braid with num_axial_wires > 0 in its reinforcement_settings (there is no separate reinforcement_type value).
layers[].reinforcement_settingsper layerNested object: ppi, num_carriers, wire_shape, wire_material_name, round_wire_diameter_mm or flat_wire_thickness_mm/flat_wire_width_mm, etc.
reinforcement_settings.num_filersbraid or coilWires running side by side within each carrier (multi-filar / multi-end). Integer ≥ 1, default 1. They widen the band a carrier lays down, so coverage saturates sooner and the weave jams at a lower PPI; they sit beside each other rather than stacking, so they add no wall thickness on their own. Spelling: the app displays "filar" (Latin filum, thread, the root of bifilar/trifilar/multifilar), but this key keeps its original num_filers spelling because it appears in every saved design. num_filars is accepted as an alias and normalized on load.
reinforcement_settings.coil_num_carrierscoilIndependent coil runs spaced evenly by the pitch (a multi-start helix). Integer ≥ 1, default 1. A separate axis from num_filers: coil geometry depends on the product coil_num_carriers × num_filers. Note this is not num_carriers, which is a braid field.
reinforcement_settings.num_axial_wirestriaxial braidNumber of straight axial (0°) wires. 0 or absent = a plain biaxial braid.
reinforcement_settings.axial_wire_*triaxial braidOptional axial-wire overrides: axial_wire_shape, axial_wire_diameter_mm (round) or axial_wire_thickness_mm + axial_wire_width_mm (flat), and axial_wire_material_name. Each defaults to the bias wire when omitted.
pullwires.pullwire_shape / round_pullwire_diameter_mm / pullwire_thickness_mm+pullwire_width_mm / pullwire_material_nametop levelThe pullwire WIRE cross-section and material (global). Drives the physical tendon stretch stiffness k = E·A/L; material unset = legacy inextensible tendon.
layers[].pullwire_lumenpullwire layerPer-section lumen/liner overrides: inner_diameter_mm (round), lumen_thickness_mm+lumen_width_mm (flat), add_liner, liner_wall_thickness_mm, liner_material_name. Absent keys fall back to the legacy global pullwires values; lumen inner dimensions are floored at the wire dimension + 0.001".
catheter_definition.core_profiletop levelOptional multi-lumen extruded core (see the core section): a solid polymer rod with shaped lumens replacing the round mandrel bore. Keys: enabled, template_id, template_params, od_mm, material_name, lumens (materialised geometry: the single source the mechanics consume), optional rotation_deg (90° steps). Requires dimension_mode: "ID_FIXED".
sections[].use_coreper sectionOn a core design, false opts this one section out of the core (legacy round-bore path). Defaults to true.

Starting the API

Run the compiled executable with the --cli-api argument:

./VirtuCath.exe --cli-api

Installation Note: If you installed VirtuCath using the standard Windows setup wizard, the executable's location depends on your installation choice. If installed for "All Users", it is located at C:\Program Files\VirtuCath\VirtuCath.exe. If installed for "Current User Only", it is in %LocalAppData%\Programs\VirtuCath\VirtuCath.exe. You must navigate to this directory in your terminal or add it to your system PATH to run the commands below.

Once running, the API process waits silently for JSON commands on stdin. Logs are written to api_cli.log in the per-user log directory (%LocalAppData%\VirtuCathLLC\VirtuCath\logs\ for an installed build) to keep stdout clean.

Verbose mode: Append the --verbose flag (e.g., ./VirtuCath.exe --cli-api --verbose) to stream internal simulation engine logs to standard error (stderr). Useful when debugging physics divergence or command failures.

Gym mode: Append --gym-mode to lift the rate limit for parametric RL / optimization sweeps. Available on any paid plan; trial licenses stay metered, with a small daily export allowance for evaluation. See Gym mode.

Usage protocol

  1. Send a single line of valid JSON to stdin. This can be a single command object {...} or an array of command objects [{...}, {...}] for batch execution. Note: Even when sending an array, the entire payload must be compacted onto a single string line without embedded newlines.
  2. Terminate the JSON string with a newline (\n).
  3. The program processes the command and responds with a single line of JSON on stdout, followed by a newline. If sending a batch array, it will respond with a single line of JSON per command.
  4. All commands are validated against an internal JSON Schema before execution.
  5. If a command causes an error (or fails schema validation), the response will be explicitly structured to include an "error_code" for deterministic programmatic handling.

Example Error Response (Schema Validation):

{
"status": "error",
"error_code": "SCHEMA_VALIDATION_ERROR",
"message": "Command failed validation: 'timeout_s' must be of type number",
"path": ["timeout_s"]
}

Security Note: All inputs are strictly parsed as JSON and type-casted internally. Do not use Python eval() or send arbitrary code.

Rate limiting

The API is rate limited with a token bucket. This protects the single-threaded simulation engine from being flooded and throttles sustained high-volume automated sweeps.

  • The bucket holds a burst capacity of 120 tokens and refills continuously at 2.0 tokens per second.
  • Each command costs one token, except heavier design-space commands which cost more: run_doe costs 20, since it does a full sweep per call; the two finite-element solves, run_kink_simulation and run_push_simulation, cost 10 each; and generate_reports and generate_graphics cost 5 each. These are costed for the CPU they burn, not as extraction vectors: at one token apiece, a single burst would queue hours of solving.
  • Discovery/meta commands are exempt and never count against the budget: export_schema, get_license_info, and exit/quit.

When the bucket is empty, a command is rejected with error_code RATE_LIMITED and a retry_after_s field: the number of seconds until enough tokens have accrued for that command. Wait that long and resend.

{
"status": "error",
"error_code": "RATE_LIMITED",
"message": "Rate limit exceeded for 'run_doe'. Retry after 5.00s.",
"retry_after_s": 5.0
}

Interactive design work and RL-style step_simulation loops stay well under the limit; only sustained batch traffic (for example, large automated DOE sweeps) will hit it. Note that responses are Generated Outputs under the EULA §2.8: the API may be used for machine-learning training and dataset generation on your own designs, but not to extract a surrogate of the underlying models. Limits are configured under CLI_API.rate_limit in internal_config.

Gym mode

Parametric RL and optimization sweeps.

A parametric sweep (a "virtual gym" that varies the construction and reads the export_rod_spec output back on every iteration) runs the loop loadexport_rod_spec at machine speed. Under the default token bucket that loop throttles after a few dozen designs, which is far too slow for a real training run.

Gym mode lifts the bucket for exactly this use. Start the API with --gym-mode:

./VirtuCath.exe --cli-api --gym-mode

What you get, by license

LicenseGym modeexport_rod_spec
Any paid plan (active)Granted: bucket liftedNo quota (still metered by the token bucket unless --gym-mode is passed)
TrialNot availableCapped at 25 per day
Expired / suspended / not activatedNot availableRefused: LICENSE_REQUIRED

Gym mode is available on every paid tier: there is no separate add-on to buy.

On a trial, you can still exercise the full integration: load a config, export a rod spec, feed it into your simulator, and confirm the numbers are what you expect. The daily cap is sized to prove the pipeline works, not to complete a training run. Past the cap, export_rod_spec returns:

{
"status": "error",
"error_code": "TRIAL_QUOTA_EXCEEDED",
"message": "Trial licenses are limited to 25 rod spec exports per day (used 25). The allowance refreshes at 00:00 UTC and does not reset when the session is restarted; a paid license removes the cap and enables --gym-mode for full parametric runs.",
"quota": 25,
"used": 25,
"period": "day",
"resets_at": "00:00 UTC"
}
The trial allowance is daily and persists across restarts

The tally is stored in your user profile (<user_data_dir>/trial_quota.json), so quitting and relaunching the CLI does not refill it, and neither does winding the system clock backwards. It refreshes on its own at 00:00 UTC, so you can come back and evaluate again the next day; you just can't accumulate a dataset in one sitting.

Unlike RATE_LIMITED, this is not time-based back-off: do not retry it in a loop. Check gym_mode.trial_rod_spec_used via get_license_info before starting a batch.

Two limits that stay in force even in gym mode

  1. run_doe remains metered. It emits a dense design-space grid in a single call, so it stays on the token bucket on every plan. Drive your sweep with loadexport_rod_spec instead; that is the path gym mode is built for.
  2. step_simulation remains wall-clock limited. The dynamic engine never advances faster than real time. Gym mode does not change this, and does not need to: rod spec extraction is static and does not step the simulation.

Checking your entitlement before a long run

get_license_info reports the resolved state in a gym_mode block, so a harness can assert it got the throughput it expected before committing to a multi-hour job:

{
"status": "success",
"license_info": { "type": "Team", "status": "Active", "...": "..." },
"gym_mode": {
"requested": true,
"active": true,
"denied_reason": null,
"license_class": "paid",
"metered_commands": ["run_doe"]
}
}

If --gym-mode was requested but not granted, active is false, denied_reason explains why, and a warning is written to stderr. The session still runs (it just stays metered), so a script that always passes the flag degrades in throughput rather than crashing. On a trial, the block additionally carries trial_rod_spec_quota, trial_rod_spec_used, trial_quota_period and trial_quota_resets_at.

Export volume is recorded in api_cli.log for every session.


API commands

Most commands need a model in memory and return MODEL_NOT_LOADED otherwise. The exceptions, which work on a bare session, are export_schema, export_config_schema, load, validate_config, get_materials, add_temporary_material, create_blend, braid_transfer, get_license_info and wizard_step. The machine-readable list is x-command-preconditions in the exported schema.

export_schema

Returns the full JSON Schema defining every command, its expected parameters, required fields, and types. Use this as the runtime source of truth for what the API accepts, particularly useful for zero-prompt AI agents discovering the surface dynamically.

Beyond the command enum and per-command parameters, the schema carries two extension keys worth reading: x-command-preconditions (which commands need a loaded model) and x-error-codes (the set of error_code values, so a caller can switch on them deterministically).

This is the schema of the command envelope only. The catheter config document that load and validate_config carry inside it has its own schema, published by export_config_schema below.

Request:

{
"command": "export_schema"
}

Response (Success):

{
"status": "success",
"schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "VirtuCath CLI API Request",
...
}
}

export_config_schema

Returns the JSON Schema for the catheter config document itself — the .json design format that load and validate_config accept (schema_version 3.0.0) and that the application writes when saving a design. Where export_schema describes the command envelope, this command describes the document that travels inside it.

The config_schema in the response is the exact schema object the loader validates against (JSON Schema draft 2020-12) — exported from the validator, not maintained as a copy, so it cannot drift from what is actually enforced. Everything the loader enforces outside JSON Schema rides beside it as x- extension keys, so the schema itself stays a faithful statement of what schema validation alone will and will not catch:

  • x-validation-passes — which validation passes each entry point runs. Highlights: load with a file path injects schema defaults, then runs JSON Schema, then the cross-field rules; load with an inline object runs no document validation at all (validate first); validate_config with an inline object validates the raw document with no defaults, so required-but-defaulted keys (schema_version, sections[].is_rigid, ...) must be written out explicitly on that path.
  • x-minimal-document — a verified smallest document the file-load path accepts; every other field defaults. Note pullwires.num_tendons defaults to 1 (steerable), and a steerable design must name distal_attachment_section_id, so a minimal document either sets num_tendons to 0 or supplies that id.
  • x-custom-validation-rules — the cross-field rules enforced in code and reported as CONFIG_INVALID: unique section_ids, the distal_attachment_section_id requirement, the pullwire radial-extent check, and the multi-lumen-core rules (core section): dimension_mode: "ID_FIXED", a finite positive od_mm, the fixed_inner_diameter_mm mirror, 90° rotation_deg steps, manual_props finiteness, the lumen manufacturability guardrails, and the pullwire-vs-core clearance policy.
  • x-load-normalization — what the loader rewrites on the way in: defaults injection, _m/_mm mirroring, tendon_list regeneration, the num_filarsnum_filers alias.
  • x-save-behavior — the strict allowlist of section keys the application's save path persists. Calculated keys (outer_diameter_mm, failure metrics, pullwire_offset_mm, ...) are stripped on save, so do not expect them in files you read back.
  • x-authoring-notes — rules the Setup Wizard and wizard_step enforce at input time that the document validator deliberately does not (the lumen ≥ wire + 0.001" floor, the 2 mm / 1500 mm length limits, at-least-one-layer per non-rigid section, material names existing in the library). A hand-written file that breaks them still validates, and either fails at build time or silently means something else.

Also worth knowing before hand-writing files: additionalProperties is deliberately permissive, so a misspelled optional key is silently ignored rather than flagged — validate_config cannot catch typos in optional keys.

Request:

{
"command": "export_config_schema"
}

Response (Success):

{
"status": "success",
"config_schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "VirtuCath Catheter Configuration Document",
"type": "object",
"properties": { ... },
"x-validation-passes": { ... },
"x-custom-validation-rules": { ... },
...
}
}

load

Loads a catheter configuration into the simulation engine and initializes the mechanics. The document format config must follow is published by export_config_schema.

Request:

{
"command": "load",
"config": "C:/path/to/catheter.json"
}

Note: config can be an absolute file path, a path relative to the working directory, or a complete nested JSON object representing the configuration. Absolute paths are highly recommended.

Dimension Mode Priority Note: When passing a raw configuration JSON directly to the CLI, a dimension_mode of ID_FIXED or OD_ID_FIXED makes fixed_inner_diameter_mm the central lumen for the internal layer flowdown, in preference to mandrel_diameter_mm. A legacy config that names an ID-driven mode but carries no fixed_inner_diameter_mm still falls back to the mandrel diameter.

Configuration Validation Note: When a configuration is loaded from a file path, its pullwire geometric constraints are validated: the radial extent of the pullwires (radial_offset_mm + half the lumen dimension + liner_wall_thickness_mm, where the lumen is liner_inner_diameter_mm for round wires and pullwire_thickness_mm plus clearance for flat/ribbon) must stay inside the outer radius of the section named by pullwires.distal_attachment_section_id. The check runs only when pullwires are defined and that attachment id is set.

Passing a nested JSON object instead of a path bypasses the config-file schema, its default injection, and this check. Validate with validate_config first, or load from a file.

Where the pull-wire lumens sit (1.5.0): for a section that has a flagged pull-wire layer (layers[].is_pullwire_layer), hydration places the lumens at that layer's mid-radius (its flowdown-resolved radii), the same rule the setup wizard's cross-section picture uses, clocked one at the top, two left/right, four on the cross, and writes the radius back per section as pullwire_offset_mm (a calculated field, stripped on save) so the composite calculator, the steering model's tendon sites and the Kink Radius section model all use one position. pullwires.radial_offset_mm is only the fallback for a section with no flagged layer. Two sections never consult a layer stack: a rigid section is always placed at mid-wall, and a manual-stiffness section uses its own manual_pullwire_offset_mm, falling back to mid-wall when that is unset.

Multi-Lumen Core Note: Configs may define a multi-lumen extruded core (see Multi-lumen extruded core). Core configs are validated on load: the lumen geometry must pass the manufacturability guardrails, and, by policy, pullwires are never routed through the core, so the pullwire lumen (including any liner wall) must clear the core OD or the load is rejected with CONFIG_INVALID. Both of those hold on the file and inline-object paths. The additional requirement that dimension_mode be ID_FIXED is enforced only when loading from a file.

Response (Success):

{
"status": "success",
"message": "Catheter loaded successfully.",
"static_kpis": {... }
}

The static_kpis block here is the raw engine payload. The pullwire_configuration block described below is added only by get_static_kpis.

get_static_kpis

Retrieves the static mechanical properties (e.g., Sectional Stiffness, Tensile Failure Load, Burst Pressure) of the currently loaded model. You can optionally request specific keys to minimize the payload size.

Retired in 1.5.0

critical_buckling_loads (Euler with an imperfection knockdown) and the closed-form kink family (elastic_instability_radius_mm, elastic_instability_moment_Nm, kink_radius_mm, kink_material_radius_mm and their kink_* companions, plus ovalization_threshold_70pct_mm) are no longer part of this payload. A single per-section number cannot see the free length and end conditions (buckling) or the bend direction, lumens and layer strains (kink); the stepped Pushability and Kink Radius tools in the application answer those questions from the same model. Callers that filtered on those keys receive an empty selection.

Note: If is_manual_stiffness is flagged for a section in the configuration, certain geometric KPIs derived from the cross-section (like display_linear_density_kg_per_m) are bypassed and will return null.

Multi-lumen core sections additionally report core_min_bend_radius_mm and core_yield_moment_Nm (the plastic first-yield bend limit), plus the principal bending range (EI_min_Nm2 / EI_max_Nm2 / principal_axis_angle_deg, inside sectional_stiffness); asymmetric sections (>1% anisotropy) also carry the device-axis pair EI_x_Nm2 / EI_y_Nm2 there. The single-round-bore failure modes (burst_pressure_mpa, crush_pressure_atm) return null for core sections: those formulas assume a thin-walled round tube, which a solid extruded core is not.

Request:

{
"command": "get_static_kpis",
"keys": ["sectional_stiffness"]
}

Note: The keys array is optional. If omitted, all static KPIs are returned.

Response (Success):

{
"status": "success",
"static_kpis": {... }
}

The payload carries more than the headline KPIs. sectional_stiffness[] gives one entry per section (id, label, EI_Nm2, GJ_Nm2, EA_N, axial_compliance_mm_per_N, torsional_compliance_deg_per_mNm, is_manual, layer_types), alongside the whole-catheter sums total_axial_compliance_mm_per_N and total_torsional_compliance_deg_per_mNm. This command also appends a pullwire_configuration block (num_tendons, radial_offset_mm, shape, control_axes[], and a per-section list of pullwire_offset_mm); see the control-axis note for how those axis labels relate to bend directions.

get_dynamic_kpis

Retrieves the current state of the simulation without advancing time. You can optionally request specific keys.

Request:

{
"command": "get_dynamic_kpis",
"keys": ["tip_position_mm", "physics_stable"]
}

Note: The keys array is optional. If omitted, all dynamic KPIs are returned.

Response (Success):

{
"status": "success",
"dynamic_kpis": {
"physics_stable": true,
"tip_position_mm": [4.9, 0.0, -120.5]
}
}

Note: The physics_stable flag allows agents to immediately recognize if the latest parameters caused the simulation to diverge (e.g., NaN velocities).

Axial shortening KPIs. Every get_dynamic_kpis (and step_simulation) response also carries section_shortening_mm (dict keyed by section id), total_shortening_mm, and axial_dof_active. These report the EA-based axial compression of the shaft under pullwire tension ("lost motion"), measured from the axial slide-joint displacements. Axial compression is on by default (MJCF_GENERATION.axial_dof_enabled); set the VC_AXIAL_DOF=0 environment variable to fall back to the pre-1.2.2 rigid-axial behavior, in which case the values are 0.0 and axial_dof_active is false. Sections distal to the pullwire attachment section always report 0.0 (no pullwire load path past the anchor). Values are quasi-static: read them after the state has settled.

Ovalization and bend-regime KPIs. Every get_dynamic_kpis (and step_simulation / settle) response carries ovalization_diameters_mm, keyed by section id: the elliptical od_major_mm / od_minor_mm / id_major_mm / id_minor_mm at the section's current bend, plus a regime label (linear or pre_instability) with its provenance. Read regime_source before trusting regime: closed_form means the label and ratio_kappa_op_to_inst come from the analytic elastic-instability estimate (Brazier limit point with the shipped imperfection knockdown, linear below 0.70 of it), which can sit up to ~3x off the 2D section FE with a design-dependent sign; kink_sim means run_kink_simulation has measured this section and the label is the measured verdict, with ratio_kappa_op_to_inst recomputed against the measured collapse onset and limit_state carrying the worst measured event passed (ok / yield / lumen / self_intersect / buckle, the same vocabulary as limit_states). The dimensions themselves are the closed-form Brazier estimate and assume pure bending: tendon axial compression is not included (measured effect on the FE collapse onset: about +1% per 10 N, toward earlier collapse). A load clears measured limits; re-run the kink simulation on the new design.

Pullwire stretch KPIs. Responses also carry pullwire_stretch_mm (dict keyed by wire index, matching the tendon-force indices) and pullwire_stretch_active. Each value is the elastic elongation of that pull wire under its current tension, from the wire's physical stiffness k = E·A/L (wire cross-section and material from catheter_definition.pullwires: pullwire_shape, round_pullwire_diameter_mm or pullwire_thickness_mm/pullwire_width_mm, and pullwire_material_name). Pullwire stretch is on by default (MJCF_GENERATION.pullwire_stretch_enabled; VC_PW_STRETCH=0 falls back to the pre-1.2.2 inextensible tendon). Wires without a specified size and material use the legacy rigid tendon and report only its tiny elastic give. The actuation displacement cap automatically grows by the measured absorption (shaft compression + wire stretch), so full articulation stays reachable; responses carry actuation_cap_mm (the effective per-axis cap, null when unbounded) and actuation_cap_active (whether the last command was clamped by it). Responses additionally carry pullwire_yield_force_N: the wire's tensile capacity (tensile strength × cross-section, null for legacy wires); tension at or above it means the physical wire would yield, and the GUI turns the tension readout red. Quasi-static: read after settling.

set_environment

Enables or disables gravity along a specific axis (x, -x, y, -y, z, -z).

Request:

{
"command": "set_environment",
"gravity_enabled": true,
"gravity_axis": "-z"
}

Note: both fields are optional. gravity_enabled defaults to false and gravity_axis to z.

Response (Success):

{
"status": "success",
"message": "Gravity set to enabled along axis '-z'"
}

reset_simulation

Resets the simulation state to neutral/zero-deflection and flushes accumulated kinematics without reloading the model config from disk. Useful for rapidly starting a new sequence in bounded optimization loops.

Request:

{
"command": "reset_simulation"
}

Response (Success):

{
"status": "success",
"message": "Simulation reset successfully.",
"dynamic_kpis": {... }
}

set_proximal_fixture

Emulates a bench fixture (or anatomy) that holds the proximal shaft straight, isolating deflection to the distal tip. Every catheter joint at or proximal of distance_from_tip_mm is locked straight live (no model rebuild) while the free distal length keeps deflecting under the existing pullwire tension. The distance can be slid cheaply between or during step_simulation calls.

Behavior change (default on). The proximal fixture defaults to enabled at 150 mm from the tip. For catheters longer than ~150 mm, dynamic results differ from releases ≤ 1.1.x unless you disable the fixture ({"enabled": false}) or set the design's simulation_settings.proximal_fixture block. Catheters shorter than the distance lock zero joints (unaffected). Defaults and per-design values live in simulation_settings.proximal_fixture in the catheter config.

Request:

{
"command": "set_proximal_fixture",
"enabled": true,
"distance_from_tip_mm": 150.0
}

Note: enabled is required. distance_from_tip_mm is optional, defaults to 150.0 mm, and has a minimum of 0. ring in the response is null when the fixture is disabled or the ring lands at the base.

Response (Success):

{
"status": "success",
"proximal_fixture": {
"enabled": true,
"distance_from_tip_mm": 150.0,
"total_length_mm": 300.0,
"boundary_arc_from_base_mm": 150.0,
"locked_joint_count": 150,
"ring": { "body_id": 54, "z_local_m": 0.003, "od_m": 0.003 }
}
}

The proximal_fixture block is also included in every get_dynamic_kpis response.

step_simulation

Advances the physics simulation by a precise duration. Useful for algorithmic agents that need to observe continuous motion. Optionally accepts target X/Y deflections to update before stepping.

Real-time limiter: step_simulation is throttled to wall-clock speed: the simulation is not allowed to advance faster than real time. A request to step 10 seconds will take at least 10 seconds of wall time to return. Use small duration_s values (e.g., 0.01–0.1) for interactive loops. For bulk static-design exploration that does not require dynamic state, prefer run_doe or recompute static KPIs via repeated load calls.

duration_s is optional, defaults to 0.01 s, and must be a finite number in (0, 60]. Anything else is rejected with SCHEMA_VALIDATION_ERROR.

Request:

{
"command": "step_simulation",
"duration_s": 0.01,
"x_mm": 5.0,
"y_mm": 0.0
}

Note: x_mm and y_mm are optional, but must be supplied together. Omit both to step toward the last known target; supplying only one is silently ignored.

Response (Success):

{
"status": "success",
"message": "Stepped 0.01s.",
"dynamic_kpis": {... }
}

Errors: MODEL_NOT_LOADED; SCHEMA_VALIDATION_ERROR (a duration_s outside (0, 60] or not finite); INTERNAL_ERROR if the integrator throws, in which case the model is auto-reset and the response carries "crashed": true. Note that this path returns INTERNAL_ERROR, not the SIMULATION_CRASHED that set_pullwires uses for the same class of failure.

render_simulation

Captures a static frame of the current simulation state headlessly.

Request:

{
"command": "render_simulation",
"view": "custom",
"azimuth": 45.0,
"elevation": -30.0,
"distance": 0.5,
"lookat": [0.0, 0.0, -0.2],
"width": 800,
"height": 600,
"output_path": "C:/path/to/save/simulation_frame.png"
}

Note: output_path is the only required parameter, and absolute paths are highly recommended. view can be iso (the default), front, side, top, or custom. Even with custom, azimuth, elevation and distance remain optional and fall back to 45.0°, -30.0°, and an auto-framed distance of 1.5 × the catheter length. lookat is optional and defaults to the catheter mid-point. Image size defaults to 800x600.

Response (Success):

{
"status": "success",
"message": "Simulation rendered to /path/to/save/simulation_frame.png"
}

Errors: MODEL_NOT_LOADED; MISSING_PARAMETER without output_path; INTERNAL_ERROR for an unrecognised view or a path that cannot be written.

set_pullwires

Dynamically actuates the pullwires to achieve a specific spatial deflection at the catheter tip and steps the physics simulation until the tip settles (velocity drops below a threshold) or times out. Returns the dynamic KPIs of the settled state.

API Ambiguity Note: The x_mm and y_mm parameters refer to the target spatial position (deflection amplitude) of the distal tip in the simulation world, not the linear displacement (length) of the tendon pulled.

Note on Deflection Modeling: The catheter base is fixed. The proximal shaft fixture (on by default at 150 mm from the tip; see set_proximal_fixture) rigidly holds the shaft straight proximal of a set distance, so you can model the full-length catheter and isolate deflection to the distal tip. Disable it or increase the distance to let the whole shaft deflect.

Request:

{
"command": "set_pullwires",
"x_mm": 5.0,
"y_mm": 0.0,
"timeout_s": 15.0
}

Note: timeout_s is optional and defaults to 10.0 seconds of simulation time.

Response (Success):

{
"status": "success",
"message": "Pullwires set and simulation settled.",
"dynamic_kpis": {
"is_settled": true,
"tip_position_mm": [4.9, 0.0, -120.5],
"tendon_forces_N": { "0": 1.2, "1": 0.0,... },
...
}
}

Note: If the tip fails to settle within timeout_s, the response still returns status: "success" but with message: "Simulation reached timeout before settling." and dynamic_kpis.is_settled: false. Check is_settled rather than status to determine whether the tip actually reached steady state.

Error Codes:

CodeCondition
MODEL_NOT_LOADEDload or wizard_step has not been called yet.
SCHEMA_VALIDATION_ERRORx_mm or y_mm is missing or not a number.
SIMULATION_CRASHEDThe physics integrator diverged: a NaN/Inf tip position, or the tip flying more than 10 m from the origin. The simulation is auto-reset before this error returns; the response includes "crashed": true.
RATE_LIMITEDThe token bucket is empty; the response carries retry_after_s. See Rate limiting.
INTERNAL_ERRORAny other unhandled failure inside the handler.

calculate_flowdown

Interacts with the Flowdown Engine to iteratively calculate pre-reflow and post-reflow stackup dimensions.

Important Conceptual Note: The Reflow Calculator is a secondary, manufacturing-focused tool. It is designed to be used after the catheter geometry is modeled and simulated to determine what raw, pre-reflow extrusions to procure in order to achieve the desired final design. It is not strictly required for the physics simulation itself, which runs off the final composite stackup.

This command allows AI agents to optimize designs by passing in manual layer overrides (e.g., fixed wall thicknesses or inner diameters).

Request:

{
"command": "calculate_flowdown",
"section_index": 0,
"layer_overrides": {
"0": { "is_manual": true, "fixed_wall_mm": 0.05 },
"1": { "is_manual": false, "gap_below_mm": 0.2 }
}
}

Note: section_index defaults to 0. layer_overrides uses the layer index as the key. Available override properties are is_manual, fixed_wall_mm, fixed_id_mm, gap_below_mm, and gap_above_mm. To change the base mandrel size, set mandrel_diameter_mm in the top-level catheter_definition of your loaded configuration. By default, polymer layers have a 0.15mm radial pre-reflow gap below them to simulate loose fitment prior to reflow which affects final OD if not actively accounted for. Override gap_below_mm to 0.0 to enforce a strict flush fit.

Multi-lumen core sections: the core is the reflow boundary. The stackup seeds at the core OD (not a round mandrel), the innermost row is labeled as the core, and the core itself never reflows: walls build outward from it, though the first layer above it still carries the standard 0.15 mm clearance gap. In an ID_FIXED build, a section that carries no explicit outer_diameter_mm has its own computed final OD used as the target, so meets_target is always true for it; a section that pins an outer_diameter_mm is still judged against that.

Response (Success):

{
"status": "success",
"stackup": [
{
"name": "Mandrel",
"pre_od_mm": 1.5,
...
},
...
],
"target_od_mm": 2.0,
"calculated_final_od_mm": 1.98,
"od_delta_mm": -0.02,
"meets_target": true
}

Errors: MODEL_NOT_LOADED; INTERNAL_ERROR for a non-integer layer_overrides key or an out-of-range section_index (note that an out-of-range index is not reported as SCHEMA_VALIDATION_ERROR).

generate_graphics

Headlessly renders catheter illustrations to image files, so an agent can get visual feedback without a GUI. It writes at most two files: braid_render.png for the first Braid or Coil layer found anywhere in the configuration, and peelaway_render.png for section 1. It does not render every layer or every section.

Request:

{
"command": "generate_graphics",
"output_dir": "/path/to/save/images"
}

Note: The model must be loaded first. The directory will be created if it does not exist.

Response (Success):

{
"status": "success",
"message": "Graphics generated successfully.",
"saved_files": [
"/path/to/save/images/braid_render.png",
"/path/to/save/images/peelaway_render.png"
]
}

get_materials

Retrieves the full list of materials available to this session: the local material library plus anything added since the process started with add_temporary_material or create_blend. Each entry carries an origin of "library" or "temporary", which is how you tell a curated material from one this session invented. Note that a blend reports "library"; see the persistence note under create_blend.

Request:

{
"command": "get_materials"
}

Response (Success):

{
"status": "success",
"materials": [
{
"name": "SS 304V",
"material_family": "Metal",
"thermoplastic": false,
"density_g_cm3": 8.0,
...
},
...
]
}

add_temporary_material

Adds a custom material to the library in-memory only for the duration of the API session. This material can then be referenced by name in subsequent calculate_flowdown or load commands without permanently altering the user's materials.json file.

Parameters. material (object, required) carries the properties below. force (boolean, default false) is required to shadow a name that already exists in the curated library: without it, a colliding name is rejected with MATERIAL_EXISTS. Re-sending a name you added earlier in this session merges rather than replaces, so a field you omit keeps its previous value.

Validation. material.name is required and non-empty; density_g_cm3, modulus_of_elasticity_mpa and tensile_strength_mpa must all be greater than 0; poissons_ratio must lie strictly between −1 and 0.5. Any of youngs_modulus_e2_mpa and shear_modulus_g12_mpa that you supply are range-checked too, and NaN, infinity and booleans are rejected. Only fields actually present are checked.

Request:

{
"command": "add_temporary_material",
"material": {
"name": "Custom Agent Polymer",
"material_family": "PEBA",
"thermoplastic": true,
"density_g_cm3": 1.05,
"modulus_of_elasticity_mpa": 150.0,
"tensile_strength_mpa": 45.0,
"poissons_ratio": 0.45
}
}

Response (Success):

{
"status": "success",
"message": "Temporary material 'Custom Agent Polymer' added in memory.",
"material": {... },
"shadowed_library_material": false
}

The message reads updated instead of added when the name already existed in this session, and gains " (shadows a library material for this session)" when force overrode a curated entry. shadowed_library_material is always present.

Errors: MISSING_PARAMETER, MATERIAL_EXISTS (name collision without force), SCHEMA_VALIDATION_ERROR (a property out of range, or an unknown/duplicate field), INTERNAL_ERROR.

create_blend

Creates a new blended material from two parents (or polymer + additive) using the same mechanics model as the GUI's Create Polymer Blend / Polymer + Additive dialog. The resulting material is added to the in-memory material library for the session (it is not persisted to materials.json) and can immediately be referenced by name in subsequent load, wizard_step, or calculate_flowdown commands.

Either parent can be referenced by library name (string) or supplied as an inline material object, mirroring the dialog's "Custom..." additive workflow.

Request (both parents from the library):

{
"command": "create_blend",
"base_material": "PEBAX® 2533 SA 01 MED",
"additive_material": "Barium Sulfate (BaSO4)",
"base_mass_fraction": 0.80,
"blend_mode": "additive"
}

Request: inline custom additive (equivalent to the dialog's "Custom..." entry):

{
"command": "create_blend",
"base_material": "PEBAX® 2533 SA 01 MED",
"additive_material": {
"name": "MyCustomFiller",
"density_g_cm3": 4.5,
"modulus_of_elasticity_mpa": 30000.0,
"tensile_strength_mpa": 20.0,
"poissons_ratio": 0.30
},
"base_mass_fraction": 0.80,
"blend_mode": "additive",
"name": "PEBAX 2533 + 20% MyCustomFiller"
}

Parameters:

FieldRequiredTypeDescription
base_materialYesstring | objectBase polymer. A library name, or an inline material object with at least name, density_g_cm3, modulus_of_elasticity_mpa, poissons_ratio.
additive_materialYesstring | objectAdditive (or second polymer for blend_mode = "blend"). Same accepted shapes as base_material.
base_mass_fractionYesnumberMass fraction of the base material in the resulting blend (0.05–0.95). The additive receives 1 - base_mass_fraction.
blend_modeNostringBoth modes compute modulus with the same immiscible-blend law; blend_mode changes only the tensile strength. "additive" (default) treats additive_material as a rigid/lubricious filler and applies a knockdown that drops strength with filler fraction. "blend" treats both parents as miscible polymers and uses a volume rule of mixtures.
nameNostringOverride the resulting material name. Must not collide with an existing library entry. If omitted, a name is auto-generated from the parents and ratio.

Inline material validation (same constraints as the GUI dialog): density_g_cm3 > 0, modulus_of_elasticity_mpa > 0, tensile_strength_mpa > 0 if provided, and −1 < poissons_ratio < 0.5.

Response (Success):

{
"status": "success",
"message": "Blend 'PEBAX 2533 + 20% MyCustomFiller' created and added to in-memory library.",
"material": {
"name": "PEBAX 2533 + 20% MyCustomFiller",
"material_family": "Blend",
"thermoplastic": true,
"density_g_cm3": 1.184,
"modulus_of_elasticity_mpa": 15.51,
"poissons_ratio": 0.442,
"tensile_strength_mpa": 27.79
}
}

Error Codes:

CodeCondition
MISSING_PARAMETERbase_material, additive_material, or base_mass_fraction not provided.
SCHEMA_VALIDATION_ERRORblend_mode is not "blend"/"additive", base_mass_fraction out of [0.05, 0.95], or name is an empty string.
NAME_CONFLICTA material with the resulting name already exists in the in-memory library.
INTERNAL_ERRORA library lookup failed, an inline material was invalid (missing fields, bad units, out-of-range Poisson's ratio, etc.), or the blend calculation itself raised. The message describes which input failed.

Persistence note: Like add_temporary_material, create_blend only mutates the in-memory library for the active session, and the user's materials.json on disk is untouched. get_materials reports a blend with origin: "temporary", the same as any other session material. To persist a blend across sessions, use the GUI's Materials Library dialog: saved blends serialize as ordinary Material entries with material_family: "Blend" and reload through the standard library path with no schema additions.

get_license_info

Retrieves read-only information about the active application license, including Hardware ID, expiration status, and license type.

Request:

{
"command": "get_license_info"
}

Response (Success):

{
"status": "success",
"license_info": {
"type": "Team License (1 Year)",
"status": "Active",
"expires_at": "2027-10-31",
"days_remaining": "365 days",
"hwid": "UUID:12345678-ABCD-...",
"key_masked": "••••••••-A1B2C3D4"
},
"gym_mode": {
"requested": true,
"active": true,
"denied_reason": null,
"license_class": "paid",
"metered_commands": ["run_doe"]
}
}

Parsing license_info. days_remaining is a string, not a number: it can read "365 days", "Expired", "Unlimited" (a lifetime key) or "N/A", and expires_at can read "Never (Lifetime)" or "N/A", so a caller that coerces either to a number will break on a perpetual license. status is one of Active, Expired, Suspended, Grace Period Over, Not Activated, Invalid, or Trial Active. Two further keys appear situationally: offline_grace_remaining (days left before an online re-check is required) and, on a trial, trial_id.

The gym_mode block reports whether the --gym-mode rate-limit bypass was requested and granted for this session; see Gym mode. license_class is one of paid, trial, or none. On a trial the block also carries trial_rod_spec_quota, trial_rod_spec_used (today's spend, read from the persisted tally), trial_quota_period and trial_quota_resets_at, so a harness can check its remaining allowance before starting a batch rather than discovering it mid-run. This command is exempt from rate limiting, so it can be polled freely.

generate_reports

Generates the full analysis report bundle for the currently loaded configuration and saves it to the specified directory. The bundle always includes catheter_report.json (raw config + static KPIs) and, when generation succeeds, the PDF and Excel analysis reports. If dynamic simulation data has been logged, a simulation_log.csv is also written. PDF/Excel generation failures are non-fatal: the JSON dump still succeeds and saved_files reflects what was actually written.

Request:

{
"command": "generate_reports",
"output_dir": "/path/to/save/reports"
}

Note: The model must be loaded first. The directory is created if it does not exist.

Response (Success):

{
"status": "success",
"message": "Reports generated successfully.",
"saved_files": [
"/path/to/save/reports/catheter_report.json",
"/path/to/save/reports/catheter_report.pdf",
"/path/to/save/reports/catheter_data.xlsx"
]
}

Unified report: choosing sections

Add blocks and the command switches to the unified report: one document assembled from the sections you name, through the same block writer the application's Generate Report dialog uses. Omit blocks and nothing changes: the four-file bundle above is still what you get.

{
"command": "generate_reports",
"output_dir": "/path/to/save/reports",
"blocks": ["design", "push"],
"format": "pdf"
}
KeyContentAvailable after
designDesign Summary + Catheter Analysis: sections, layers, materials, pull-wires, cross-sections, peel-awaysload: always available
pushInputs, KPIs, per-section tightest bend radius reached, plus the full stroke curve as a spreadsheet sheetrun_push_simulation
kinkEvery section simulated this session, its bend direction, and each event radiusrun_kink_simulation

blocks is a list of those keys or the string "auto", which selects every section that has results. format is "pdf", "excel" or "both" (default). output_path names a single file directly and requires a single format. Sections always come out in the report's canonical order, whatever order you list them in.

rsm (Design Sweep) is retired from the report as of 1.5.0. The block was built around one output metric, so it could not represent a sweep over several KPIs. "auto" no longer selects it even when run_doe results are waiting, and naming it returns a SCHEMA_VALIDATION_ERROR saying it is not currently exported, why, and how to get it back: no file is written, and it is never silently dropped from a document you asked for it in. It stays a recognised key rather than becoming an unknown one, so an existing script gets a real answer instead of a typo message. Set VC_RSM_REPORT=1 before starting the CLI to restore the previous behaviour exactly. The Design Sweep tool is unaffected: run_doe still returns the full design-point frame and surface fit.

simulation (Simulation Snapshot) is retired from the report as of 1.5.0: its readouts were whatever the sliders happened to be at the moment Generate was pressed, which does not fit a document whose every other section is a run with a stated scenario. VC_STEERING_REPORT=1 restores it in the application. For this command nothing changes and the flag makes no difference: simulation was never buildable headlessly. The retirement message is reserved for keys this command could otherwise have produced, which today means rsm alone.

A headless report has tables, not plots. The application's figures (the force–stroke curve, the deflected shapes, the deformed cross-sections, the response surface) are grabbed from live plot widgets, and there is no widget here. Every block that would have carried one says so in its notes, so a reader is never left wondering whether a chart failed to render or was never there. All the underlying numbers are present.

The application's report has eight sections; three have no headless equivalent. simulation, flowdown and braid are built from live tool pages, so there is no page to ask when there is no window. analysis is not a gap either: the application folds it into the design block, which is exactly what this command builds.

blocks accepts all eight section keys, but only four are buildable here: design, push, kink and rsm. The other four are accepted by the schema so the command can answer why rather than rejecting them as unknown words. Naming analysis explains that it is not a gap but part of the design block and tells you to ask for design; naming simulation, flowdown or braid says the section is built from a live tool page and has no headless equivalent. Naming one of the four buildable keys whose run has not happened is a tailored error naming the command to run first, rather than a silently thinner report. The canonical section order is design, push, kink.

validate_config

Performs pre-flight validation checks on a configuration without loading it into the physics engine. Useful for verifying geometric constraints like ensuring layer stackups do not exceed the specified overall outer diameter. The document schema itself, together with the exact per-entry-point pass matrix (file path vs inline object), is published by export_config_schema.

status vs valid: The two fields mean different things.

  • status is "success" whenever the command itself ran without an exception. A schema-malformed input or missing config parameter would return status: "error" instead.
  • valid is true only when the configuration passed every validation check. A configuration that loads cleanly but contains warnings will return status: "success" and valid: false, plus a warnings array describing what's wrong.

Agents looking for "did the config pass?" should branch on valid, not status.

The response also carries an advisories array (1.5.1). Advisories never affect valid: they report what load-time normalization would fill in (a derived overall_diameter_mm, a braid pattern derived from pattern_display, a corrected num_layers, material-name alias resolutions) and authoring smells that stay loadable for legacy files (a braid or coil thickness_mm below the woven wire stack, an unknown material name that would hydrate as a generic 100 MPa placeholder, an ID_FIXED overall_diameter_mm that disagrees with the stackup-derived value, a mandrel_diameter_mm that disagrees with fixed_inner_diameter_mm). Treat them as fix-this-before-you-trust-the-KPIs feedback.

Request:

{
"command": "validate_config",
"config": "C:/path/to/catheter.json"
}

Response (valid config):

{
"status": "success",
"valid": true,
"message": "Configuration is valid.",
"warnings": []
}

Response (loadable but with warnings):

{
"status": "success",
"valid": false,
"message": "Configuration loaded but has validation warnings.",
"warnings": [
"Section '1': Sum of layer thicknesses (5.200 mm) exceeds overall_diameter_mm (4.000 mm)."
]
}

Pass config as a file path to get the full core validation: dimension_mode must be ID_FIXED, the lumen geometry must pass the manufacturability guardrails (containment, overlap, outer wall, web), and the pullwire-clearance policy (no pullwires through the core) is enforced. An inline config object is checked against the JSON schema only, so none of those three run on that path.

The stackup check is strictly-over only: a stack that hits overall_diameter_mm exactly is valid. A 1e-9 mm float-noise guard protects that equality; it is not a design tolerance, and a real 1 micron overshoot still fails. The check seeds the stack at the same bore hydration uses: fixed_inner_diameter_mm in the ID-driven modes, the mandrel otherwise.

Two authoring requirements were relaxed in 1.5.1 for hand-written documents. pullwires.radial_offset_mm is only required on a steerable design (num_tendons > 0); a num_tendons: 0 document may omit the key. And in ID_FIXED mode overall_diameter_mm may be omitted: the loader derives it from the stack (bore + 2 × layer thicknesses of the thickest section).

Balancing the jacket (balance_last_layer, opt-in). Add "balance_last_layer": true to the request to have each section's outermost thermoplastic jacket solved so the post-reflow OD lands exactly on overall_diameter_mm, and to validate that stackup instead. Without the flag nothing changes. This is the CLI counterpart of the GUI wizard's Adaptive layer, computed with the same reflow model hydration uses at load.

  • The response adds adjustments (per section: old_thickness_mm, new_thickness_mm, od_before_mm, od_after_mm, or a skipped reason for rigid, layer-less, or core sections) and adjusted_config, a copy of the config whose only edits are the solved thicknesses. Pass adjusted_config straight to load.
  • A section whose outermost layer cannot absorb the remainder (a braid, coil, pull-wire carrier, heat shrink, or non-thermoplastic layer, or inner layers that already fill the target) returns valid: false with the reason in errors.
  • An ID-fixed (inside-out) build returns BALANCE_NOT_APPLICABLE: there the OD is an output of the stack, not a target.

calculate_kink_safety

Retired in 1.5.0. This command computed the ratio of a target bend radius to each section's closed-form kink / collapse radius. That analytical kink family has been retired from the application, the reports and this API, so the command now returns an error instead of a number:

{
"status": "error",
"error_code": "COMMAND_RETIRED",
"message": "calculate_kink_safety was retired in 1.5.0 together with the closed-form kink radii it was built on ..."
}

Use run_kink_simulation instead: it bends the real cross-section step by step and reports the radii the section actually failed at. To reproduce the old "is radius R safe?" question, compare your target against kink_sim_first_failure_R_mm; a target larger than that value is safe, since these are bend radii and bigger is gentler. Multi-lumen core sections also still report their plastic first-yield bend limit (core_min_bend_radius_mm) in get_static_kpis.

run_kink_simulation

Bends one section's real cross-section to a progressively tighter radius as a 2D finite-element ramp, and reports the centerline bend radius at which each failure event occurs. This is the headless route to a section's bend limit, and the replacement for the retired calculate_kink_safety. Requires a loaded model. section_index is 0-based.

It uses Design Sweep's screening preset and event definitions, so a value here and a sec{N}_kink_sim_* column from run_doe are the same measurement. The Kink Radius tab shares the definitions but solves a finer mesh and a longer ramp (96+ elements around against 48, twenty ramp steps against twelve, five refinements against two), so its radii can differ; where they disagree, the tab is the better estimate.

bend_direction is optional and defaults to 0°. It selects the loading direction from the same label set run_doe offers in kink_bend_directions; which labels a design distinguishes depends on its pull-wire clocking, core symmetry and bending anisotropy, and a label this design does not distinguish is rejected with the ones it does. The response echoes both bend_direction and bend_angle_deg.

This one is slow. Every other KPI command is closed-form and returns in milliseconds; this is a real FE ramp taking seconds to a minute for a single section. It costs 10 rate-limit tokens. To sweep a design space use run_doe with include_kink_simulation, which parallelises across sections and design points.

Request:

{
"command": "run_kink_simulation",
"section_index": 0,
"ovalization_bend_radius_mm": 20.0
}

ovalization_bend_radius_mm is optional (default 10x the design OD) and sets the reference bend at which the *_at_R_* fields report the section's shape.

The results object carries the seven event radii in millimetres of centerline bend radius:

FieldMeaning
kink_sim_first_failure_R_mmEarliest permanent event: the larger of the 50 % lumen and first-crease radii. The number to design to.
kink_sim_collapse_onset_R_mmBending-moment peak (Brazier limit point): the onset of collapse.
kink_sim_lumen50_R_mm50 % loss of undeformed lumen area: the ISO 25539-2 / EN 13868 kink definition.
kink_sim_first_crease_R_mmFirst layer to reach its crease strain.
kink_sim_collapse_onset_bound_R_mmThe onset as an upper bound, not a measurement: the ramp ended before the moment peaked, so onset is known only to be at or inside this radius. Mutually exclusive with kink_sim_collapse_onset_R_mm: at most one of the pair carries a number, and both are null when no onset was found at all.
kink_sim_snap_through_R_mmEnd of the stable elastic path. Not the ISO kink and not a permanent event, so it does not feed first_failure; it is the fallback collapse onset when nothing peaked.
kink_sim_lumen_closure_R_mmMinor lumen ID below 5 % of its original: the bore has effectively shut. Strictly tighter than lumen50, with its own severity tier.

first_failure stays frozen as max(lumen50, first_crease): folding the last three into it would change the meaning of every sweep already on disk. Those three exist so a headless caller can rebuild the same section limits the Catheter Analysis tab shows: they were computed on every run before 1.5.0 and discarded before the columns were written.

A field is null when the ramp never reached that event. Treat null as "not measured", never as zero.

Mandrels. A bench test wraps a pin, not a radius, so the same events come back a second time in results_mandrel as the mandrel diameter that produces them: mandrel = 2R − OD, keyed kink_sim_*_mandrel_mm, with the section's own OD in section_od_mm and radius_convention fixed at "centerline". Only the event radii are restated, and only those larger than half the OD (a radius at or inside OD/2 would give a zero or negative mandrel). The ovalization axes are not: 2 × OD_major − OD is not the mandrel of anything. The radii remain the primary form and the only one used by run_doe columns: a mandrel depends on the OD, so it cannot be compared across designs of different diameter, and it collapses to zero at R = OD/2.

{
"results": { "kink_sim_first_failure_R_mm": 8.3, "...": null },
"results_mandrel": { "kink_sim_first_failure_mandrel_mm": 13.21 },
"section_od_mm": 3.39,
"radius_convention": "centerline"
}

Runs can stop early. The response carries a run_status object (completed, stop_reason, stopped_at_R_mm, detail, section_od_mm, core_extrusion, first_crease_layer, and bend_angle_deg on a run that solved). stop_reason is null for a clean ramp, or one of self_intersection, no_convergence, or not_run (the section could not be meshed at all, so every result is null). On a multi-lumen core the ramp stops if the cross-section is detected passing through itself: the model has no self-contact, so anything past that point is not physical. Events at or inside the stop radius are reported as null. Check run_status.completed before trusting a sweep.

The measured capacity, in section_limits. results is what the section did; section_limits is what it means, and it is the same object the application publishes to its own Steering, Navigation and Pushability warnings. It is not a rounded copy of results: buckle_R_mm is the snap radius when nothing peaked (buckle_from_snap), is null altogether for a solid multi-lumen core even when kink_sim_collapse_onset_R_mm carries a number, and lumen_R_mm falls back to the 50 % area loss when the bore never fully shut.

FieldMeaning
reached_any_eventtrue when at least one event was reached. false on a completed ramp is a real result (nothing was reached over the swept range), not missing data.
collapse_modepeak / snap / lumen / self_intersect / undetermined / not_applicable. Six values, kept apart because a single null was flattening incompatible meanings: only undetermined means we did not find out, and lumen ("it ovalises shut rather than kinking") is a real and rather good answer.
yield_R_mm, yield_layerPast permanent set, and which layer gave way first.
buckle_R_mm, buckle_is_bound, buckle_from_snapCollapse onset. buckle_is_bound means it is an upper bound, not a measurement: the moment was still rising when the solver stopped.
snap_R_mm, snap_is_boundThe snap radius: the tightest bend the section still held a stable shape at, bisected to within 0.5 % of the radius. snap_is_bound means the bisection could not get there and the value is quantised to the bend ramp: the true snap lies at or inside it, anywhere in the ramp step that contains it, and identical snap radii across designs that share a ramp are then an artefact of the grid, not agreement.
lumen_R_mm, lumen_is_boundWhere the lumen shuts, interpolated between the two solved steps that bracket the crossing. lumen_is_bound means the lumen was already past its threshold at the first solved step, so the value is the start of the ramp and the event really occurs at some larger radius the ramp never solved. That is the non-conservative direction, unlike the other two bound flags: never design to a flagged lumen_R_mm.
unreachable_inside_R_mmNothing at or inside this radius was observed: the section passed through itself there.
bend_angle_deg, is_default_orientationThe orientation it was measured at.
severity_order, notesThe state vocabulary least-to-most severe, and the caveats that apply to this result.
Orientation is part of the answer

A capacity measured at a non-default rotation is a different load case, not the same number slightly off: the shipped single-pull-wire design moves 2.7 % between 0° and 180°, because collapse starts on the compressive flank even though bending stiffness cannot tell the two apart.

"Measured, nothing reached" is not "never run"

"section_limits": null means the section could not be simulated at all. A real block with reached_any_event: false means it ran and reached nothing: a measured result, and a good one. limit_states[].state still reads not_measured in the second case, because the shared vocabulary has no separate token for it; the block's notes says so.

Is this bend safe? Pass demand_bend_radius_mm (one centerline radius in mm, or a list of them) and limit_states judges each against the capacity using the application's own rules, so you never re-derive them from the raw radii. A list is accepted because the solve is the expensive part and the classification is free.

{
"command": "run_kink_simulation", "section_index": 0,
"demand_bend_radius_mm": [50.0, 8.0, 3.0]
}
{
"limit_states": [
{"demand_bend_radius_mm": 50.0, "state": "ok", "severity": 1, "capacity_R_mm": null, "description": "is within its measured limits"},
{"demand_bend_radius_mm": 8.0, "state": "yield", "severity": 2, "capacity_R_mm": 25.7322, "description": "is bent past permanent set in Jacket"},
{"demand_bend_radius_mm": 3.0, "state": "buckle", "severity": 5, "capacity_R_mm": 3.5614, "description": "is bent past collapse onset"}
]
}

state is ok / yield / lumen / self_intersect / buckle / not_measured: the same token the on-screen warnings switch on, in that severity order. buckle tops it on an engineering call: permanent set may knowingly be accepted in a single-use device, a shut lumen stops it working, and an aggressive kink is not something you design around. There is no proximity tier: a section at 5 % margin reports ok exactly as one at 200 % does. limit_states is null, never an empty list, when no demand was asked for or there is no capacity to judge against.

run_push_simulation

Pushes the catheter and reports where it buckles: the headless counterpart of the Pushability tab, tortuous-track fixture included. Requires a loaded model. Before this command existed the whole tool was GUI-only, with no scripted route to a push force, a buckling onset or a per-section bend radius.

This one is slow. A real nonlinear finite-element solve, seconds to a minute, like run_kink_simulation and unlike every closed-form KPI command. It costs 10 rate-limit tokens.

Two experiments, not two settings of one. Without track you get a straight vessel (or an open chamber): the clamped free length is shoved along its own axis, tip_restraint says what the tip meets, and channel_diameter_mm adds vessel walls. With track you get a bench trackability run: the device is gripped in a straight introducer at the fixture's mouth and fed through the groove, so the engaged length grows from nothing. The fixture supplies its own channel width (so channel_diameter_mm must be omitted) and the tip meets nothing (so tip_restraint is ignored).

stroke_mm is required for a straight push (the stroke is the experiment, so there is no default) and is capped at half the free length, past which the rod is folding double rather than being pushed. With a track it is ignored: a fed run stops at whichever runs out first, the device, the track, or feed_length_mm, and scenario.stroke_mm echoes your value back unused. feed_length_mm is optional and caps how far the device is fed; omit it to feed as far as the fixture allows.

Request:

{
"command": "run_push_simulation",
"free_length_mm": 150.0,
"stroke_mm": 8.0,
"channel_diameter_mm": 4.0,
"tip_restraint": "wall",
"tip_shape": "conical",
"tip_cone_length_mm": 6.0
}

tip_restraint is one of free, pinned, guided, clamped, wall; tip_shape (wall mode only) is conical, round or flat, and tip_cone_length_mm is the length of a conical taper back from the apex: real geometry, because over the cone the shaft is narrower than its OD and needs less vessel. Friction is mu_tip (default 0.20) and mu_channel (default 0.03). Call export_schema for the exhaustive field list.

Leave n_elems alone unless you have a reason. Omitted, the mesh is sized from the vessel clearance and the track's tightest bend so the buckle wavelength is resolved. A coarse mesh silently locks into a lower buckling mode and still satisfies every convergence check: the run passes its own free-body audit and the answer is still wrong. When the element cap leaves the mesh coarser than the fixture, notes says so and gives the count that would resolve it.

The track object is either a preset ({"preset": "astm_based", "channel_width_mm": 2.8} or {"preset": "serpentine", ...}) or an explicit fixture in the track file format: an object with a segments list of {"kind": "straight", "length_mm": N} and {"kind": "arc", "radius_mm": N, "angle_deg": A} plus channel_diameter_mm. Positive angles turn left. A fixture authored in the application and saved to disk can be pasted straight in.

astm_based is based on ASTM F2394: a run through it is not a conformance test. The published fixture is a branched network; this is one traced route through it, with both approach straights halved. It is a representative tortuous path with the right character, and its name says so wherever it appears. serpentine is deliberately generic. The whole fixture scales with its groove width, so a wider channel brings proportionally larger bends: the same test on a bigger device, rather than the same bends in a wider slot.

Response. results carries peak_push_N, peak_tip_normal_N, push_force_at_end_N, tip_normal_at_end_N, max_lateral_deflection_mm, peak_wall_contact_N, buckling_knee_stroke_mm / buckling_knee_force_N, and euler_reference_N / euler_K.

FieldMeaning
buckling_knee_*The buckling onset: where the push force first stops rising. null means the curve never flattened over the stroke swept: the column did not buckle. That is a result, not a gap.
euler_reference_NIdeal stepped-column Euler load for the chosen restraint, as a sanity reference only. null in wall mode and in a fed track run, where the restraint is emergent.
peak_wall_contact_NLargest load any one contact patch puts on the wall, not the largest single-node force, which is a mesh artefact.
sections[].min_bend_radius_mmThe tightest centerline bend radius that section actually reached over the whole push. null covers both "it never bent, so there is no finite radius" and "it owns no node distal of the clamp, so nothing was measured": do not read a null as a pass.
scenarioEvery input as resolved, including defaults, and scenario.track is the fixture as solved in the same format the request takes. A push number without the fixture that produced it is not a result.
run_statusquality is the run verdict: clean (every step settled), qualified (the buckling knee and peak force are resolved; the curve past first_unsettled_stroke_mm is not), or unresolved (the audit fails at or before the knee; treat the forces, including the knee, as unresolved). converged / unconverged_steps report the same per-step physical audit (free-body check on the final solved shape, after the solver's rescue passes), with first_unsettled_step / first_unsettled_stroke_mm locating the first failure and force_residual_frac_max / moment_residual_frac_max giving the worst imbalance as numbers. solver_exit_clean_steps is the raw optimiser exit count, diagnostic only. equilibrium_ok / equilibrium_failed_steps are the same audit collapsed over the run (kept for compatibility), plus steps_solved, track_fed_mode and feed_stop_reason (reported for every fed run, including unconverged ones).

Compare sections[].min_bend_radius_mm against kink_sim_first_failure_R_mm from run_kink_simulation for the same section: one is the bend the push demanded, the other is the bend the section can take.

Set include_curve: true for the per-stroke-step arrays; they are n_steps long and most callers want the KPIs. Results are held for the session, so generate_reports can fold this run into a report as its push block.

braid_transfer

Predicts how a regular braid changes when it is cut off a spool mandrel and stretched down onto a target shaft, or, when a goal is given, solves the spool PPI required to hit a target braid after transfer (goal seek). Pure geometry: no load is required, and the wire material does not affect the result. The braid is defined with the same reinforcement_settings keys used in catheter configs; Coil layers and triaxial braids (num_axial_wires > 0) are not supported. This is the headless counterpart of the Braid Transfer Calculator window; the model conserves wire length and helix turns at the nominal wire centerline, so enter measured, relaxed (as-received) spool values for non-heat-set braid.

Request (forward transfer):

{
"command": "braid_transfer",
"braid": {
"ppi": 60,
"num_carriers": 16,
"pattern": "One Wire Under - Over Two (Full Load)",
"wire_shape": "Flat/Ribbon",
"flat_wire_thickness_mm": 0.0762,
"flat_wire_width_mm": 0.127,
"wire_material_name": "304V (1/2 Hard)"
},
"spool_diameter_mm": 4.0,
"target_diameter_mm": 1.0
}

Response (Success):

{
"status": "success",
"message": "Forward transfer computed.",
"mode": "forward",
"spool": { "surface_diameter_mm": 4.0, "ppi": 60.0, "pitch_mm": 3.3867, "angle_deg": 75.45, "coverage_percent": 52.4, "fill_factor": 0.310, "status": "OK", "centerline_diameter_mm": 4.1524 },
"target": { "surface_diameter_mm": 1.0, "ppi": 15.65, "pitch_mm": 12.982, "angle_deg": 15.58, "coverage_percent": 49.8, "fill_factor": 0.291, "status": "OK", "centerline_diameter_mm": 1.1524 },
"wire_length_per_wrap_mm": 13.4776,
"elongation_ratio": 3.833,
"spool_per_finished": 0.2609,
"transferable_od_range_mm": [0.189, 4.124],
"warnings": []
}

elongation_ratio is the finished braid length per unit of spool braid length (> 1 when necking down); spool_per_finished is its inverse: the purchasing number. transferable_od_range_mm is the span of shaft ODs this spool braid can reach before the weave binds (fill factor 0.94, the same threshold behind the wizard's Binding Likely (Jamming) status) at either the necking or the hoop end, or null if the braid binds at every diameter. status uses the wizard's density language (OK, High Density (Process Limit), Near Binding, Binding Likely (Jamming)). Multi-filar braids (num_filers > 1) are counted in full as of 1.2.4: the extra wires per carrier enter coverage, fill and every published bound, so a multi-filar braid binds on a larger shaft than a single-filar one with the same carrier count. Before 1.2.4 they were excluded and the response carried an optimism warning; that warning no longer exists. Set VC_MULTIFILAR=0 to restore the old carriers-only behaviour.

Request (goal seek): add a goal object: the braid's own ppi is then ignored and the required spool PPI is solved instead. parameter is one of ppi, angle (degrees from the longitudinal axis), or coverage (percent).

{
"command": "braid_transfer",
"braid": { "num_carriers": 16, "pattern": "One Wire Under - Over Two (Full Load)", "wire_shape": "Flat/Ribbon", "flat_wire_thickness_mm": 0.0762, "flat_wire_width_mm": 0.127 },
"spool_diameter_mm": 4.0,
"target_diameter_mm": 1.5,
"goal": { "parameter": "ppi", "value": 16.0 }
}

Response (Success):

{
"status": "success",
"message": "Goal seek solved.",
"mode": "goal_seek",
"required_spool_ppi_exact": 47.81,
"required_spool_ppi_int": 48,
"spool_max_ppi": 189.4,
"spool": { "...": "spool braid at the exact required PPI" },
"target": { "...": "goal braid at the target diameter" },
"spool_at_int_ppi": { "...": "spool braid at the integer machine PPI" },
"target_at_int_ppi": { "...": "what the integer PPI actually achieves at the target" },
"elongation_ratio": 2.988,
"spool_per_finished": 0.3346,
"transferable_od_range_mm": [0.189, 4.201],
"warnings": []
}

required_spool_ppi_exact is validated against spool_max_ppi (the spool's own jamming limit); braiders set whole-number PPI, so required_spool_ppi_int and the *_at_int_ppi readbacks show the consequence of rounding before ordering. target_at_int_ppi is null (with a warning) if no integer PPI reaches the target.

Error Codes:

CodeCondition
SCHEMA_VALIDATION_ERRORbraid, spool_diameter_mm, or target_diameter_mm missing or malformed.
TRANSFER_IMPOSSIBLEThe transfer violates wire-length conservation: the target is geometrically unreachable (braid angle would exceed 90°). The message reports the maximum reachable OD (forward) or the maximum spool OD for the goal (goal seek).
INVALID_INPUTTriaxial braid supplied, non-positive dimensions, or a goal value outside its domain (e.g. a coverage goal below the minimum achievable at the target diameter).

get_config

Retrieves the full JSON configuration object currently loaded in the simulation engine. This allows an AI agent to inspect the current state (or a default load), tweak specific parameters (like a layer thickness or pullwire offset), and re-issue a load command without having to reconstruct the entire document from scratch.

Request:

{
"command": "get_config"
}

Response (Success):

{
"status": "success",
"config": {
"schema_version": "3.0.0",
"catheter_definition": {
"name": "MyCatheter",
...
}
}
}

export_rod_spec

Exports the loaded device as a versioned rod_spec, the reduced per-section constitutive properties (bending, torsion and axial stiffness, outer diameter, linear density) in SI, plus optional pullwire actuation geometry, for import into third-party physics models (NVIDIA Isaac Cosserat rod, MuJoCo, custom FEA/XPBD). Only the reduced constitutive outputs are emitted; never the composite internals that produced them (braid, ply moduli, flowdown stackup). See Physics export (rod_spec) for the full schema and adapters.

Requires a loaded model. Optional parameters:

  • path: if given, also writes the JSON to this file path.
  • resample_ds_m: if given, includes a fixed-step resampled array (in addition to the piecewise-constant sections) at this arc-length spacing in metres.
Running this in a parametric loop

Varying the construction and re-exporting on every iteration is the intended "virtual gym" workflow. Start the API with --gym-mode to lift the rate limit on that loop, available on any paid plan. On a trial, export_rod_spec is capped at 25 per day (TRIAL_QUOTA_EXCEEDED), enough to validate the output format and your integration but not to complete a training run. That allowance persists across restarts and refreshes at 00:00 UTC. See Gym mode.

export_rod_spec requires an active license or a live trial. With neither, it returns LICENSE_REQUIRED and retrying will not clear it: start VirtuCath once to begin the 15-day trial, or activate a license. Every other CLI command remains available.

Request:

{
"command": "export_rod_spec",
"path": "device.rod_spec.json",
"resample_ds_m": 0.001
}

Response (Success):

{
"status": "success",
"rod_spec": {
"schema": "virtucath.rod_spec/1.0",
"provenance": { "virtucath_version": "1.2.0", "config_hash": "sha256:...", "units": "SI" },
"device": { "name": "MyCatheter", "total_length_m": 0.32, "proximal_is_s0": true, "transitions": "step" },
"sections": [
{
"s_start_m": 0.0, "s_end_m": 0.20, "label": "Proximal",
"EI_x_Nm2": 1.70e-3, "EI_y_Nm2": 1.70e-3,
"GJ_Nm2": 1.74e-3, "EA_N": 2831.1,
"r_out_m": 0.001694, "lin_density_kg_m": 8.996e-3
...
}
],
"actuation": { "type": "pullwire", "pullwires": [ ... ] }
},
"warnings": [],
"written_to": "device.rod_spec.json"
}

warnings is an empty array when the export passes the internal round-trip validator. written_to is present only when path was supplied. The actuation block is present only for a steerable device.

wizard_step

Guides an AI agent through building a new catheter configuration step by step: the scripted equivalent of the Setup Wizard. It manages an internal state machine (Global Setup → Section Setup → Layer Setup → Review), whose actions are ordered by data dependency rather than by the GUI's step grouping, so a change to the wizard's layout cannot break your automation.

It validates at each step, for example rejecting a layer that would push the flowdown OD past the global OD constraint. That particular rejection does not apply to a multi-lumen core design, where the build is inside-out and the computed OD is the design OD, so there is no global-OD ceiling to exceed.

Besides the step actions below, get_status, get_state and cancel are accepted at any point. get_state returns current_state, section_index, layers_defined, valid_actions and sections_summary. Every prompting response also carries required_payload and a machine-readable payload_schema, so an agent can discover the next payload rather than hard-coding it.

Workflow:

  1. Send {"action": "start"} to initialize.
  2. Send {"action": "submit_global", "payload": {...}} with global parameters.
  3. (Optional) Send {"action": "submit_core_profile", "payload": {...}} to define a multi-lumen extruded core. Must come before the first section: the core sets the inner build basis (walls build outward from the core OD, dimension_mode becomes ID_FIXED). See Multi-lumen extruded core.
  4. Send {"action": "submit_section", "payload": {...}} to define the section parameters. On a core design, an optional "use_core": false keeps that one section on the legacy round-bore path.
  5. Send {"action": "submit_layer", "payload": {...}} iteratively for each layer (from the inside out).
  6. Send {"action": "finish_section"} to close the current section and loop back to step 4 for the next section.
  7. Send {"action": "finish_wizard"}. This automatically hydrates the built configuration, calculates KPIs, loads it into the simulation engine, and returns the full "static_kpis" just like a standard load command.

submit_core_profile payload:

FieldRequiredTypeDescription
od_mmYesnumberCore outer diameter in mm (> 0). Mirrored into both fixed_inner_diameter_mm and mandrel_diameter_mm.
template_idNostringOne of the wizard templates: ring_n, central_satellites (default), double_d, tri_pie, round_crescent, central_crescents, oval, custom_props.
template_paramsNoobjectThe template's parametric dimensions, exactly as authored in the GUI wizard (e.g. {"n": 4, "d_mm": 0.4, "pcd_mm": 1.2} for ring_n). Omitted keys take the template defaults. Unequal lumens use d_list.
material_nameNostringCore polymer. Must exist in the library (get_materials); unknown names are rejected.
rotation_degNonumberAssembly-level core orientation; must be a multiple of 90.
manual_propsFor custom_propsobjectHand-entered section properties: A_mm2, Ixx_mm4, Iyy_mm4, Ixy_mm4, J_mm4.

The response echoes the built core (lumen_count, ...), a guardrails array (the manufacturability report: outer wall / web / containment / overlap, each with a level), and warnings. Geometry with any error-level guardrail is rejected with SCHEMA_VALIDATION_ERROR, exactly like the GUI blocks Next on an invalid profile. If pullwires were already submitted and would overlap the core, a warning tells you the minimum radial_offset_mm before finish_wizard/load rejects the config.

Request (Example core submission):

{
"command": "wizard_step",
"action": "submit_core_profile",
"payload": {
"template_id": "ring_n",
"od_mm": 2.0,
"material_name": "PEBAX® 3533 SA 01 MED",
"template_params": { "n": 4, "d_mm": 0.4, "pcd_mm": 1.2 }
}
}

Request (Example Layer Submission):

{
"command": "wizard_step",
"action": "submit_layer",
"payload": {
"name": "Braid Layer",
"material_name": "PEBAX 35D",
"reinforcement_type": "Braid",
"thickness_mm": 0.1,
"reinforcement_settings": {
"ppi": 80,
"num_carriers": 16,
"wire_shape": "Round",
"wire_material_name": "SS 304V",
"round_wire_diameter_mm": 0.05
}
}
}

For a triaxial braid, submit the layer exactly as a Braid and add num_axial_wires (and optionally the axial_wire_* keys) to reinforcement_settings:

"reinforcement_settings": {
"ppi": 80,
"num_carriers": 16,
"wire_shape": "Round",
"wire_material_name": "SS 304V",
"round_wire_diameter_mm": 0.05,
"num_axial_wires": 8
}

For a multi-filar braid (several wires loaded side by side on each carrier), add num_filers. The braid below has 16 carriers each carrying 2 wires, so 32 wires in total:

"reinforcement_settings": {
"ppi": 80,
"num_carriers": 16,
"num_filers": 2,
"wire_shape": "Round",
"wire_material_name": "SS 304V",
"round_wire_diameter_mm": 0.05
}

Coils separate the two axes. coil_num_carriers is the number of independent runs spaced evenly by the pitch (a multi-start helix); num_filers is how many wires ride side by side within each of those runs. The example below is a 3-start coil with 2 wires per start:

"reinforcement_settings": {
"pitch_mm": 0.5,
"coil_num_carriers": 3,
"num_filers": 2,
"wire_shape": "Round",
"wire_material_name": "SS 304V",
"round_wire_diameter_mm": 0.05
}

Both keys default to 1 and may be omitted. On a triaxial braid num_filers applies to the bias wires only: axial wires are always single.

Response (Example Validation Error):

{
"status": "error",
"message": "Layer rejected. Predicted Flowdown OD (4.150mm) exceeds global target (4.000mm). Resubmit with \"balance_last_layer\": true to let this layer absorb the remainder, if it is the outermost jacket."
}

The OD gate is strictly-over only: a stack that lands on the target exactly is accepted (a 1e-9 mm float-noise guard protects the equality; a real 1 micron overshoot still rejects). The prediction is seeded at the submitted mandrel_diameter_mm (or the core OD), the same basis hydration uses, so what the gate accepts is what finish_wizard builds.

Balancing the jacket (balance_last_layer, opt-in). Computing the exact jacket wall by hand invites sub-micron misses that the OD gate rejects. Two payload switches let the outermost thermoplastic jacket absorb the remainder instead, the CLI counterpart of the GUI wizard's Adaptive layer. Both default to false; nothing changes without them.

  • submit_layer with "balance_last_layer": true: this layer's thickness_mm (still required, used as the starting guess) is solved so the section's post-reflow OD lands exactly on overall_diameter_mm. The response reports balanced_thickness_mm and predicted_od_mm. The layer must be a plain thermoplastic polymer: a braid, coil, pull-wire carrier, heat shrink, or non-thermoplastic layer is rejected with BALANCE_FAILED (and popped, like any rejected layer).
  • finish_wizard with {"payload": {"balance_last_layer": true}}: every non-rigid section's outermost jacket is solved before the config is loaded. The response adds balance_adjustments (one entry per section: old and new thickness and OD, or a skipped reason). All sections are solved before anything is committed, so BALANCE_FAILED on any section leaves the wizard state exactly as submitted, like WIZARD_INCOMPLETE.

On a core design (or any ID-fixed build) there is no OD target to hit, and the flag returns BALANCE_NOT_APPLICABLE.


Multi-lumen extruded core

A catheter may replace the single round mandrel bore with a multi-lumen extruded core: a solid polymer rod with shaped lumens (holes), on top of which the wall stack (jackets, braids, coils, pullwire layers) is built. The core drives the cross-section mechanics, including anisotropic bending for asymmetric profiles, and is fully supported across the API.

Config shape (catheter_definition.core_profile):

{
"core_profile": {
"enabled": true,
"template_id": "ring_n",
"template_params": { "n": 4, "d_mm": 0.4, "pcd_mm": 1.2 },
"od_mm": 2.0,
"material_name": "PEBAX® 3533 SA 01 MED",
"lumens": [ { "shape": "Round", "diameter_mm": 0.4, "x_offset_mm": 0.6, "y_offset_mm": 0.0 } ],
"manual_props": null,
"rotation_deg": 90
}
}

Key semantics:

  • lumens is the single geometry source. The template + params document how the profile was authored; the materialised lumens list is what the mechanics consume. The easiest way to produce a valid pair is wizard_stepsubmit_core_profile (which builds lumens from the template for you) or the GUI wizard.
  • Dimension basis: core designs are dimension_mode: "ID_FIXED": walls build outward from the core OD, and the final OD is a computed result. od_mm is mirrored into both fixed_inner_diameter_mm and mandrel_diameter_mm. Set both when hand-authoring a core config: calculate_flowdown reads mandrel_diameter_mm for any section with use_core: false, so omitting it leaves those opt-out sections building on a stale default bore.
  • Per-section opt-out: sections default to the core; "use_core": false on a section keeps it on the legacy round-bore path.
  • Pullwire policy: pullwires are never routed through the core. They exist only via the legacy pullwire pathway (catheter_definition.pullwires + pullwire layers) and run in the wall stack above the core. Load/validate enforce the clearance.
  • Templates: ring_n (1–8 lumens on a pitch circle), central_satellites (central + 0–8 satellites), double_d (2–4 D-lumens with area-balance knobs), tri_pie (pie sectors), round_crescent (tangent round + crescent remainder), central_crescents (central + 2–4 crescent sections), oval, and custom_props (hand-entered A/Ixx/Iyy/Ixy/J).

Where the core shows up across commands:

CommandCore behavior
load / validate_configFull validation: guardrails, ID_FIXED basis, pullwire-clearance policy.
get_static_kpisAdds core_min_bend_radius_mm, core_yield_moment_Nm, EI min/max + principal angle; burst / crush return null.
wizard_stepsubmit_core_profile authors a core from a template (guardrail-checked).
calculate_kink_safetyRetired in 1.5.0 (returns COMMAND_RETIRED); use run_kink_simulation.
run_kink_simulationMeshes the core itself (lumens, webs, septa), so core sections are fully supported, but the ramp stops early if the section is detected passing through itself. Check run_status.completed.
calculate_flowdownThe core is the reflow boundary: stackup seeds at the core OD; the core never reflows.
run_doeCore hydrates on every design point and core KPIs are collected, but core_profile paths cannot be swept (fixed tooling).
generate_reportsPDF and Excel include the core construction card (template + parametric dimensions) and core bend-limit rows.

DOE / Design Sweep commands

The following two commands expose the Design Sweep programmatically. A catheter configuration must be loaded first (via load or wizard_step) before calling these commands.

run_doe

Runs a headless Design of Experiments (DOE) sweep over one or two continuous design parameters (with an optional A/B categorical split attribute). For each design point, the static KPIs are recalculated from scratch using the composite mechanics models and the results are aggregated into a response surface. A cubic polynomial is then fitted to the specified output metric and evaluated on a fine 10-point-per-axis grid, which is returned with the response. The fitted coefficients are not exposed and there is no predict command, so interpolate that grid for intermediate points.

Design Matrix:

  • 1 variable: 5-point linear sweep (min, 25%, mid, 75%, max).
  • 2 variables: 5×5 full factorial, 25 runs covering every combination of the 5 levels on each axis.
  • Attribute (A/B): The entire continuous DOE is duplicated for each attribute value, doubling the run count.

If too many of the requested simulations error out to support a cubic fit, the modeler fits a simpler quadratic surface instead so you still get a usable result. The surface_model.degree field in the response tells you which was used.

Path Specification: Variables and attributes reference a location inside the config using a JSON path array. Each element is either a string key or an integer array index. The path names a leaf in the active catheter configuration, and any missing intermediate objects along it are created (so a legacy config that never carried a pullwire_lumen block can still be swept on one). Swept pull-wire lumen inner dimensions are clamped up to the wire + 0.001 in floor, so a sweep can never build a lumen smaller than its wire. Use get_config first to inspect the structure if needed.

Note: run_doe currently supports static KPI sweeps only: no live dynamic simulation per run. If you need dynamic simulation data across design points, use the GUI's Design Sweep.

Multi-lumen core paths are rejected. Any variable or attribute path containing core_profile returns SCHEMA_VALIDATION_ERROR: the core is fixed tooling (an extrusion die); its lumen layout is stored in absolute coordinates and does not rescale with a swept OD, so sweeping it would either breach the lumen envelope or silently diverge from the authored template. Sweep wall-stack parameters instead, or author a new core in the wizard. (The GUI's Design Sweep module shows the core OD read-only for the same reason.) Core sections still hydrate correctly on every design point, and the core KPIs are collected per run.

Request:

{
"command": "run_doe",
"variables": [
{
"name": "Wall Thickness",
"path": ["catheter_definition", "sections", 0, "layers", 0, "thickness_mm"],
"min_val": 0.1,
"max_val": 0.5
},
{
"name": "Braid PPI",
"path": ["catheter_definition", "sections", 0, "layers", 1, "reinforcement_settings", "ppi"],
"min_val": 40,
"max_val": 120
}
],
"attribute": {
"path": ["catheter_definition", "sections", 0, "layers", 1, "reinforcement_settings", "wire_material_name"],
"val_a": "SS 304V",
"val_b": "MP35N"
},
"output_metric": "sec0_bending_stiffness_EI_Nm2",
"output_csv": "C:/path/to/results/doe_sweep.csv"
}

Parameters:

FieldRequiredTypeDescription
variablesYesarray (1-2 items)Continuous input variables to sweep. Each item must have name, path, min_val, and max_val.
attributeNoobjectCategorical A/B split. Must have path, val_a, and val_b.
output_metricNostringKPI column to fit the cubic surface to. Defaults to "sec0_bending_stiffness_EI_Nm2".
output_csvNostringAbsolute path to save the full raw results as a CSV file.
include_kink_simulationNobooleanAlso run every section of every design point through the Kink Radius section simulation (fixed DOE resolution, several CPU threads, identical sections run once) and add the sec{N}_kink_sim_* radius columns below. Slow: seconds per section per design point. Default false.
kink_ovalization_bend_radius_mmNonumberWith include_kink_simulation: the centerline bend radius (mm) at which the sec{N}_kink_sim_*_at_R_* columns report the section's OD/ID axes and ovality. Default 10 x the design OD.
kink_bend_directionsNo"auto" or array of stringsWith include_kink_simulation: which bend directions to solve each section at. Omit for one direction. "auto" runs every direction this design makes distinct; an array runs exactly those labels. Each direction is a full extra solve per section per design point. See below.

Bend directions (kink_bend_directions):

The same cross-section bent two ways is two different solves: a pull-wire lumen on the compressed flank is not the same loading as that lumen on the stretched one. Omitting this key solves one direction, which is the whole answer for a symmetric design and an arbitrary slice of it otherwise.

A label names the direction of the section that points at the extrados, the outside of the bend: +Y puts the top of the cross-section on the outside, -Y puts it on the inside. Which labels exist is a property of the design, not a fixed vocabulary: it follows from the pull-wire clocking as it is actually meshed, the multi-lumen core's own mirror symmetries, and whether any section is meaningfully anisotropic (the same >1% test as bending_stiffness_EI_x_Nm2). A design offers between one and four of +Y, -Y, Y (symmetric), +X, -X, X (symmetric) and any (fully symmetric), where "(symmetric)" means the two opposite directions of that pair compute the same number and are run once. The shipped single-pull-wire layout offers ["+Y", "-Y", "X (symmetric)"]. A four-lumen layout collapses to ["any (fully symmetric)"] only when the stiffness is isotropic as well: an anisotropic one still separates the two bend planes, as ["Y (symmetric)", "X (symmetric)"].

Naming a label this design does not distinguish returns SCHEMA_VALIDATION_ERROR listing the ones it does, so "auto" is how to discover them. The response echoes the labels actually run in a kink_bend_directions field, in canonical order however they were requested, useful for recording what a sweep covered, though the column names already say which direction each answer belongs to.

+Y always leads that order, and it is section rotation 0: the one direction every sweep ran before this key existed, so omitting the key reproduces the historical number under the historical column name.

The plain column names mean rotation 0 and nothing else. They are kept for exactly one case: a single direction at that orientation. Ask for any other direction, alone or alongside others, and every kink column names it, so ["-Y"] returns sec{N}_yneg_kink_sim_* and no unprefixed column at all.

What was requestedColumn names
key omitted, or a single direction at rotation 0sec{N}_kink_sim_* (unchanged)
anything else, including a single non-default directionsec{N}_{slug}_kink_sim_*, slug = ypos / yneg / y / xpos / xneg / x / any; for example sec0_yneg_kink_sim_first_failure_R_mm

Cost is linear in the direction count. Each direction is a full finite-element solve of every section of every design point. Identical sections are still deduplicated, but the bend angle is part of that key, so the second direction is never served the first one's cached answer. The GUI equivalent is the Bend directions checkbox row on the Design Sweep tab, which resolves the offer through this same function.

Not the same +X as control_axes. get_static_kpis reports pullwire_configuration.control_axes in the same +X / -X / +Y / -Y alphabet, but for a different quantity: which way the tip moves when you pull that wire. A bend direction names the side of the section facing the outside of the bend, while pulling a wire puts that wire on the inside. The two are built from different tables and neither is derived from the other: control_axes is a fixed mapping convention, and the directions a design distinguishes come from its meshed lumens and its bending anisotropy. They share an alphabet, not a meaning, so do not convert between them.

Available output metrics (prefix sec{N}_ for section index N):

  • bending_stiffness_EI_Nm2
  • torsional_stiffness_GJ_Nm2
  • axial_stiffness_EA_N
  • burst_pressure_mpa
  • display_linear_density_kg_per_m
  • kink_sim_collapse_onset_R_mm, kink_sim_lumen50_R_mm, kink_sim_first_crease_R_mm, kink_sim_first_failure_R_mm, kink_sim_collapse_onset_bound_R_mm, kink_sim_snap_through_R_mm, kink_sim_lumen_closure_R_mm: only with include_kink_simulation: true: centerline bend radii (mm; the mandrel a bench wrap would use is 2R − OD for that section, and is deliberately not a column: it depends on the OD, so it is not comparable across a sweep that varies diameter) from the Kink Radius section simulation: the moment peak (Brazier limit point), 50 % lumen-area loss (ISO 25539-2 / EN 13868 kink), the first layer to reach its crease strain, the earlier of the last two, the onset upper bound when the ramp ended before the moment peaked, snap-through, and lumen closure (see the run_kink_simulation table above). NaN where the event did not occur before the ramp stopped or the section could not be meshed. The names are unprefixed as shown only for the default single direction at section rotation 0; any other kink_bend_directions request gives every one of these columns a direction slug (sec{N}_{slug}_kink_sim_*). (The closed-form elastic_instability_* / kink_* metrics were retired in 1.5.0.)
  • kink_sim_od_major_at_R_mm, kink_sim_od_minor_at_R_mm, kink_sim_id_major_at_R_mm, kink_sim_id_minor_at_R_mm, kink_sim_od_ovality_at_R_percent, kink_sim_id_ovality_at_R_percent, kink_sim_oval_ref_R_mm: only with include_kink_simulation: true: the section's OD/ID axes and ovality (100 x (1 - minor/major)) at the reference bend radius kink_ovalization_bend_radius_mm (echoed in kink_sim_oval_ref_R_mm), interpolated along the same run; NaN when the section failed before reaching that radius.
  • tensile_failure_load_N: first-yield onset: axial load at which the first material reaches its tensile strength (conservative; not the ultimate rupture load, which is higher).
  • torque_failure_Nm
  • crush_pressure_atm
  • flexural_rigidity_EI_min_Nm2 / flexural_rigidity_EI_max_Nm2 / principal_axis_angle_deg: principal bending range (multi-lumen core; NaN for legacy round-bore designs).
  • core_min_bend_radius_mm / core_yield_moment_Nm: plastic first-yield bend limit (multi-lumen core only; NaN otherwise).
  • bending_stiffness_EI_x_Nm2 / bending_stiffness_EI_y_Nm2: device-axis bending pair for asymmetric sections (>1% anisotropy; NaN for isotropic sections; EIy equals the headline EI).
  • axial_compliance_mm_per_N / torsional_compliance_deg_per_mNm: compliance pair (L/EA, L/GJ): how far the section stretches or compresses per unit axial load, and twists per milli-newton-metre of torque. Compliances add for sections in series, but the whole-catheter sums are not DOE columns: they are top-level fields of get_static_kpis, so they cannot be used as an output_metric here.

Response (Success):

{
"status": "success",
"message": "DOE complete: 50 runs, metric='sec0_bending_stiffness_EI_Nm2'.",
"total_runs": 50,
"output_metric": "sec0_bending_stiffness_EI_Nm2",
"runs_summary": [
{
"run": 1,
"x1": 0.1,
"x2": 40,
"attr": "SS 304V",
"sec0_bending_stiffness_EI_Nm2": 0.000412,
"error": null
}
],
"surface_model": {
"fitted": true,
"degree": 3,
"r2_score": 0.9987,
"adjusted_r2": 0.9978,
"residual_rms": 1.24e-5,
"predictions": {
"x1": [0.1, 0.144, "..."],
"x2": [40, 48.9, "..."],
"z": [[0.000412, "..."], "..."]
}
},
"output_csv": "C:/path/to/results/doe_sweep.csv"
}

surface_model fields:

FieldTypeMeaning
fittedbooltrue if the modeler produced a usable surface. false if too few simulations succeeded to fit one.
degreeintPolynomial degree used: 3 for the standard cubic fit, 2 when the modeler had to fall back to a quadratic.
r2_scorefloat | nullOverall goodness-of-fit (1.0 means the surface passes through every sample point).
adjusted_r2float | nullR² adjusted for the number of fitted coefficients: stays honest when the data is noisy or near-flat. Use this one when deciding whether to trust the surface.
residual_rmsfloat | nullAverage distance from the surface to the simulated points, in the metric's units. Treat predicted values as accurate to roughly ±residual_rms.
predictionsobject | nullFine-grid surface evaluation suitable for plotting (x1, x2, z).

Interpreting fit quality: adjusted_r2 ≥ 0.95 means the surface tracks the data closely and predictions are trustworthy. Between 0.70 and 0.95 the surface is usable but the underlying behavior likely has features the cubic doesn't fully capture: treat predictions directionally. Below 0.70 the surface is mostly fitting noise; rely on the raw simulated values instead. These are the same thresholds the GUI uses for its green/amber/red color tier.

Error Codes:

CodeCondition
MODEL_NOT_LOADEDload or wizard_step has not been called yet.
MISSING_PARAMETERvariables array is empty.
SCHEMA_VALIDATION_ERRORA variable or attribute path is not a non-empty array or contains core_profile; kink_ovalization_bend_radius_mm is not positive and finite; kink_bend_directions is neither "auto" nor a non-empty array, or names a direction this design does not distinguish (the message lists the ones it does).
INTERNAL_ERRORUnhandled exception during the sweep.

get_doe_results

Returns the raw per-run data table from the most recent run_doe call. This is useful to retrieve the full result set (including all KPI columns, not just the surface-fitted metric) without repeating the sweep. Results are cached in memory until the next run_doe call.

Optionally, you can filter the returned columns to reduce payload size.

Request:

{
"command": "get_doe_results",
"columns": ["x1", "x2", "sec0_bending_stiffness_EI_Nm2", "sec0_kink_sim_first_failure_R_mm"]
}

Note: columns is optional. If omitted, all columns are returned.

Response (Success):

{
"status": "success",
"last_metric": "sec0_bending_stiffness_EI_Nm2",
"total_runs": 50,
"columns": ["x1", "x2", "sec0_bending_stiffness_EI_Nm2", "sec0_kink_sim_first_failure_R_mm"],
"rows": [
{
"x1": 0.1,
"x2": 40,
"sec0_bending_stiffness_EI_Nm2": 0.000412,
"sec0_kink_sim_first_failure_R_mm": 8.3
}
]
}

Error Codes:

CodeCondition
NO_DOE_RESULTSrun_doe has not been called in this session.

exit or quit

Safely stops the simulation engine, deallocates memory, and closes the application.

Request:

{
"command": "exit"
}

Response (Success):

{
"status": "success",
"message": "Exiting CLI API."
}

(The program will then terminate)