Advanced Metal Research
GitHub Contact AMR

Weld planner HTTP API

On this page
  1. Quick start
  2. Routes
  3. Health
  4. Progress
  5. Plan
  6. Query parameters
  7. How a request runs
  8. Response
  9. A plan without a trajectory
  10. Errors
  11. Fetch a trajectory
  12. Related pages

The weld planner's motion server is a small FastAPI app. You POST a .weldplan container and get back a plan result, and, if every segment passed the verifier, the digest of a dense trajectory (.rdt) you can then fetch. The offline programming (OLP) server is its usual client. See Weld planning and verification for what the planner does.

The server listens on 0.0.0.0:8796 by default. It has no authentication, and there is no OpenAPI page (/docs is disabled).

Warning

Bind the weld planner to localhost, or firewall it. By default it listens on every network interface, and anyone who can reach port 8796 can queue GPU plans and download every stored trajectory. Start it with --host 127.0.0.1 when OLP runs on the same machine. When a Steam Deck or another host must reach it, allow only those hosts through a firewall. See Run the weld planner.

Tip

Machine-readable. The four routes, their query parameters and errors as an OpenAPI 3.1 document, generated from this page.

Quick start#

Check the GPU, plan a committed example part, and fetch its trajectory:

PLANNER=http://localhost:8796

curl -s $PLANNER/api/motion/health
# {"cuda": true, "device": "NVIDIA RTX A4000"}

# Plan with verification and a dense trajectory. This takes minutes.
curl -s -X POST --data-binary @weld_planner/v1/data/motion/bracket_a_2x.weldplan \
  -H "Content-Type: application/octet-stream" \
  "$PLANNER/api/motion/plan?run_weld_trajopt=true&run_connecting_trajopt=true&verify=true&dense=true&program_id=bracket_a_2x" \
  -o result.json

jq '.dense.trajectory_digest // .dense_error' result.json

# Fetch the verified trajectory by its digest.
DIGEST=$(jq -r .dense.trajectory_digest result.json)
curl -s -o plan.rdt "$PLANNER/api/motion/dense/$DIGEST"

The device name in the health answer is whatever your GPU reports. To make your own .weldplan, use Export .weldplan… in OLP, or POST /api/offline-programming/v1/weld-plan/export on the OLP server.

Routes#

Method and pathPurpose
GET /api/motion/healthWhether CUDA is available
GET /api/motion/progressThe running plan's current stage
POST /api/motion/planPlan a .weldplan
GET /api/motion/dense/{trajectory_digest}Fetch a stored .rdt

Health#

GET /api/motion/health

Reports whether the planner's Torch build can see a CUDA device.

200 OKJSON
{"cuda": true, "device": "NVIDIA RTX A4000"}
FieldTypeDescription
cudabooleantrue if CUDA is available
devicestring or nullThe name of device 0, or null without CUDA

Progress#

GET /api/motion/progress

The stage of the plan that is running now, for a client watching a long request.

200 OKJSON
{"stage": "verify"}

stage is null when no plan is running. While one is, it is the last stage the planner reported, for example sweep, weldseam, freespace seed, optimize or verify. There is one slot, because only one plan runs at a time.

Plan#

POST /api/motion/plan

Plans every seam and move of the posted .weldplan, verifies them, and optionally writes the dense trajectory.

The body is the raw .weldplan bytes. Options go in the query string.

Query parameters#

NameTypeDefaultRangeDescription
k_bestinteger51–16Candidate paths to keep per seam from the M4 search
screen_collisionsbooleantrueScreen search poses against the collision model, and include collision avoidance in M5 and M6. false means unchecked, not safe.
samples_per_seaminteger or nullnull2–512Space the M4 lattice by sample count. Null spaces it by time, from the weld's travel speed.
run_weld_trajoptbooleanfalseRun M5, the continuous weld trajectories. Minutes rather than seconds.
run_connecting_trajoptbooleantrueRun M6, the approach, transits, retract and taught moves
free_space_backendstringbsplinebspline, curobo, legacyThe M6 solver. curobo uses cuRobo for comparison and is optional at runtime. legacy is deprecated and kept to reproduce old results. Any other value returns 422.
verifybooleantrueRun the verifier on M5 and M6 output
verify_margin_mmnumber, mm2.00–50The verifier's clearance margin
order_seamsbooleanfalseReorder welds to shorten the transits. Off, the program's own weld order is kept.
densebooleanfalseBuild, check and store the .rdt. Needs run_weld_trajopt and run_connecting_trajopt.
program_idstring""up to 120 charactersStamped into the plan identity. A dense trajectory needs a plain identifier (letters, digits, ., _, :, -).
manifest_revisioninteger1≥ 1Stamped into the plan identity
plan_revisioninteger1≥ 1Stamped into the plan identity

The OLP server always sends run_weld_trajopt=true, run_connecting_trajopt=true, verify=true and dense=true, plus the free-space backend and the program identity.

How a request runs#

  • One plan at a time. A second request waits for the first. It can still be cancelled while it waits.
  • One process per plan. Each admitted request runs in a fresh Python process that owns the GPU. This costs interpreter, model and CUDA start-up on every plan.
  • Cancellation. If the client disconnects, the server sends SIGTERM to the planning process group, then SIGKILL after 2 s, and answers 499. A cancelled plan never stores a .rdt.
  • Atomic publication. The .rdt is written to the store only after the planning process succeeded and the client is still connected.

Response#

200 OK with the result document, schema amr-weld-planner-v1.motion-plan-result.v1. On the wire, angles are in degrees and lengths in mm; times are in s. A trimmed example:

200 OKJSON
{
  "schema": "amr-weld-planner-v1.motion-plan-result.v1",
  "cell_id": "rosie_1400_v3",
  "screened": true,
  "k_best": 5,
  "request": {"request_sha256": "3f1c0a9d2b7e…", "program_id": "bracket_a_2x", "cell_id": "rosie_1400_v3", "…": "…"},
  "producer": {"module": "amr-weld-planner/v1", "source_revision": "0123456789abcdef0123456789abcdef01234567", "source_revision_state": "bound"},
  "coverage": {"candidate_collision_screening": "sampled_lattice_nodes",
               "weld_trajectory_continuous_verification": "evaluated_for_all_emitted_trajectories",
               "connecting_trajectory_continuous_verification": "evaluated_for_all_emitted_trajectories",
               "canonical_motion_server": "not_evaluated", "physical_motion": "not_evaluated",
               "execution_authority": "none", "candidate_limit_checks": "reported_per_candidate"},
  "seams": [{
    "id": "seam_0001", "length_mm": 118.0, "crossed": true,
    "axis_names": ["J1", "J2", "J3", "J4", "J5", "J6", "J7"], "held": {"J8": 0.0, "J9": 0.0},
    "candidates": [{"cost_deg": 41.8, "joints_deg": [[…]], "times_s": […]}],
    "trajectory": {
      "knots_deg": [[…]], "knot_velocity_deg_s": [[…]], "times_s": […],
      "sampled_tracking_mm": {"knots": 0.02, "midpoints": 0.07},
      "tracking": {"verdict": "PASS", "tolerance_mm": 0.5, "bound_mm": 0.21, "worst_seen_mm": 0.08, "segments_out": 0, "undecided": 0, "unverifiable": []},
      "verdict": {"verdict": "PASS", "collisions": 0, "penetrating": 0, "limit_violations": 0, "unverifiable": [], "worst_clearance_mm": 6.412}
    }
  }],
  "connecting_trajectories": [{
    "kind": "approach", "from": null, "to": "seam_0001", "duration_s": 4.2,
    "knots_deg": [[…]], "knot_velocity_deg_s": [[…]], "times_s": […],
    "clearance_mm": {"env": 38.5, "self": 61.2},
    "verdict": {"verdict": "PASS", "collisions": 0, "penetrating": 0, "limit_violations": 0, "unverifiable": []}
  }],
  "dense": {
    "trajectory_digest": "sha256:9b1e4f07a2c3…", "plan_id": "bracket_a_2x:3f1c0a9d2b7e",
    "program_digest": "sha256:3f1c0a9d2b7e…", "dt_s": 0.01, "total_sample_count": 5230,
    "total_duration_s": 52.29, "robot_cell": {"model_id": "rosie_1400_v3", "…": "…"},
    "segments": [{"index": 0, "kind": "freespace", "source_id": "home->seam_0001", "sample_count": 421, "duration_s": 4.2}]
  },
  "options": {"k_best": 5, "verify": true, "verify_margin_mm": 2.0, "…": "…"},
  "timings_ms": {"…": 0}
}

The values above are illustrative. The fields that matter most:

FieldDescription
seams[].crossedWhether the M4 search found a path across the seam. When false, frontier and frontier_causes say where and why it stopped.
seams[].trajectoryThe M5 curve: knots_deg, knot_velocity_deg_s and times_s form a cubic Hermite, with its verdict and tracking reports
connecting_trajectories[]The M6 moves, in execution order: kind (approach, transit or retract), from and to seam ids (null at the home end), and a node_id for moves tied to a program node
connecting_trajectories_errorPresent when M6 failed. The welds may still be good.
program_orderPresent when the program has taught moves: the enabled nodes in order
speed_scalePresent when the program's plan speed is below 1. times_s are already stretched and knot_velocity_deg_s already scaled.
denseWith dense=true: the .rdt summary, including trajectory_digest, plan_id (<program_id>:<first 12 hex of the request digest>), program_digest (sha256: plus the request digest), dt_s, total_sample_count, total_duration_s, max_abs_qd_rad_s (rad/s), robot_cell, bytes and segments[]
dense_errorWith dense=true, when no .rdt was written: {reason, detail, segment_index}

See The plan result for every block, and Verdicts for what PASS, REFUSED and FAIL mean.

A plan without a trajectory#

A plan can answer 200 OK and still have no .rdt. Then dense_error says why, and the server keeps the request and result under <dense store>/refused/ (newest 5) for diagnosis. The reasons are admission's and the encoder's:

reasonMeaning
result_emptydense=true without both trajectory stages, or nothing playable in the result
identity_invalidprogram_id is empty or not a plain identifier
seam_not_plannedA seam was not crossed by the search
motion_not_verifiedA seam or move has no PASS, a non-zero collision, penetration or limit count, an unverifiable check, or a tracking certificate out of tolerance
motion_join_failedThe connecting moves could not be planned
motion_result_invalidThe result is malformed, for example duplicate seam ids or an unknown move kind
motion_not_plannedA program node has no planned motion
qd_limit_exceededA sample is faster than the cell's per-axis velocity ceiling
q_step_exceeded, boundary_qd_nonzero, boundary_q_discontinuity, time_grid_invalid, sample_count_overflow, …The resampled trajectory broke an .rdt format rule; see the .rdt format

segment_index names the failing segment when there is one.

Errors#

StatusBodyWhen
400{"error": "an empty body is not a .weldplan"}Empty body
422{"detail": "unknown free_space_backend …"}Unknown free_space_backend
422{"detail": [ … ]}A query parameter is out of range or the wrong type
422{"error": "<type>: <message>"}Planning failed, including a .weldplan the planner refused to open (bad zip, digest mismatch, wrong program schema) or missing cell meshes
499{"error": "Planning cancelled"}The client disconnected

Fetch a trajectory#

GET /api/motion/dense/{trajectory_digest}

Returns the stored .rdt bytes for a digest.

curl -s -o plan.rdt http://localhost:8796/api/motion/dense/sha256:9b1e4f07a2c3…
NameInTypeDescription
trajectory_digestpathstringsha256: and 64 lowercase hex characters, from dense.trajectory_digest

The response is application/octet-stream with Cache-Control: no-store. A malformed digest returns 422 (expected sha256:<64 hex>). An unknown digest returns 404.

The store only holds trajectories that passed admission. It lives in --dense-store-dir, else WELD_PLANNER_DENSE_STORE_DIR, else ~/.cache/rosieos-olp/weld-planner-dense, as sha256-<hex>.rdt. It is a cache: if a file is gone, plan again. OLP's Load fetches from this route and answers dense_blob_not_found when the planner no longer has the file. See the .rdt format for the file layout.