# Weld planning, verification and evidence

> How a CAD part becomes a verified joint trajectory, what the weld planner's verifier and admission check prove and do not prove, and what the plan result records.

URL: https://advancedmetalresearch.com/docs/concepts/weld-planning-and-verification
Section: RosieOS docs / Concepts
Last updated: 2026-10-10

The weld planner turns a CAD part and a program into joint trajectories that the robot can play. It then proves, against the cell model, that those trajectories are clear of collisions and inside the joint limits, and it only writes the playable file if that proof holds for every segment.

This is the only verification path in RosieOS. Nothing else in the system checks a motion against the cell geometry. Read [What is verified before motion](https://advancedmetalresearch.com/docs/get-started/safety-model#what-is-verified) in the safety model first. This page explains what that check covers.

![Three rows. Author: a STEP part goes to the seam worker, which finds weld joints and seams and searches torch angles; the program and the part are packed into a .weldplan. Plan: the weld planner runs the M4 seam search, then M5 and M6 trajectory optimisation, then the verifier, and admission writes a .rdt only if every segment passes. Load and run: Simulate optionally replays the bytes, OLP Load fetches the file by digest and checks identity and Home, rt-control admits it, and Play starts it in rosie-rt-core. Jog, moves, Home and Go to plan start are not on this path and are not verified.](https://advancedmetalresearch.com/assets/docs/weld-planning-pipeline.svg)

*Figure: The red box is the verifier: the only place RosieOS checks motion against the cell geometry. No `.rdt` exists unless every segment passes it.*

## The pipeline

1. **Author.** You import a STEP part in offline programming (OLP) and place it on the cell. The seam worker finds the weld joints, proposes seams and searches the torch work and travel angles along each one. You accept or trim each weld. Taught moves, dwells and Home can go in the same program. See [Program a weld from CAD](https://advancedmetalresearch.com/docs/guides/offline-programming).
2. **Pack.** OLP packs the program, the cell descriptor, the torch, the STEP file and any fixtures into one `.weldplan` container, with a SHA-256 digest for every member. See the [weld program format](https://advancedmetalresearch.com/docs/reference/weld-program-format).
3. **Plan.** The weld planner runs three stages on an NVIDIA GPU:
   - **M4, seam search.** A dynamic program over a sampled lattice of robot poses, including positioner angles, finds the K best ways across each seam. It screens each sampled pose against a sphere model of the arm. The result is exact within the sampled lattice. It is not a continuous check.
   - **M5, weld trajectory optimisation.** Each seam's best candidates become a continuous curve (cubic Hermite knots and velocities) that keeps the tool on the seam.
   - **M6, connecting moves.** The approach from the cell's reset pose, the transits between welds, the retract, and any taught moves.
4. **Verify.** Every M5 curve and every M6 move goes to the verifier, which judges it against the exact cell meshes.
5. **Admit and encode.** Admission reads the verifier's reports. If every segment passed, the planner resamples the plan into a dense trajectory (`.rdt`) and files it by digest. If not, no `.rdt` exists.
6. **Load and run.** OLP's Load fetches the `.rdt` by digest from the planner's store. It checks the plan's identity, the robot description and cell calibration and Home. rt-control then admits it against the live machine. See [Connect to a cell and run a program](https://advancedmetalresearch.com/docs/guides/connect-a-cell).

The search stages use a fast, approximate collision model: the arm as spheres, and everything it must not hit as signed-distance fields. The verifier deliberately uses a different model, the real triangles, so that the approximation cannot certify itself.

## What the verifier checks

The verifier works on the real triangles, in 64-bit floating point on the CPU. It has its own forward kinematics and shares no code with the planner it judges.

### Collisions

The verifier does not sample waypoints. For each segment it bounds how fast any point of each link can move. It then asks whether each pair of bodies is further apart, at the middle of an interval, than that bound allows the gap to shrink across the interval. If so, no contact exists anywhere in the interval. If not, it halves the interval and asks again. It stops after 10 halvings, and reaching that depth is a refusal, never a pass.

| Bodies | Checked against |
|---|---|
| Every robot and positioner link that has a mesh in the robot description | Every other link, except links joined by a joint, which touch by construction |
| The workpiece, tessellated from the request's STEP file and posed by the placement | The links |
| Fixtures from the request, in the cell's world frame | The moving arm links and the workpiece only |

A pair counts as a collision when it comes closer than the verification margin: 2 mm by default, set by `verify_margin_mm` (0 to 50 mm). The margin exists because the cell meshes are the visual meshes and fixtures are measured by hand. The report counts `collisions`, and separately `penetrating` for pairs that actually touch or overlap.

The verifier refuses to judge a trajectory against a cell it cannot see. If the robot's meshes are missing, planning fails with an error rather than passing.

### Limits

- **Joint positions.** Checked against each joint's limits in the cell descriptor. A joint with no position limit is reported as unverifiable.
- **Joint velocity and acceleration.** Checked against the rate limits in the cell profile, which come from the robot description's [`config.json`](/docs/reference/robot-description#config-json), narrowed by the cell. For connecting moves the check uses the exact extrema of the cubic Hermite curve that is emitted, not only the knots. A joint with no rate limit is reported as unverifiable.
- **Travel mobility, welds only.** Whether the joints can deliver the programmed travel speed along the seam direction without exceeding their rate limits. This catches poses near a singularity, where a slow tool speed would need very fast joints.

### The tracking certificate, welds only

The verifier also bounds how far the tool point can be from where it belongs on the seam, at every instant between samples, not just at the samples. The tolerance is the seam's `tolerance.position_mm`, 0.5 mm by default. The certificate is exact for straight seam segments. For a curved seam segment the bound does not exist yet, so the certificate reports it as unverifiable, and the weld cannot pass.

"Tracking" here means this geometric certificate on the planned path. There is no seam tracking or sensing in RosieOS: nothing measures the real seam while welding. See [Process I/O and sensing](https://advancedmetalresearch.com/docs/concepts/process-io-and-sensing).

### Verdicts

Each report has one of three verdicts:

| Verdict | Meaning |
|---|---|
| `PASS` | Every check ran and passed, and nothing was unverifiable |
| `REFUSED` | No failure was found, but at least one check could not run (it is listed in `unverifiable`), or the tracking search was undecided |
| `FAIL` | A collision, a limit violation or a tracking violation was found |

The distinction the verifier keeps is between "checked and clear" and "not checked". A missing rate limit is not a passed rate check.

## What admission requires

Admission is the gate between the planner's result and the `.rdt` file. It is `require_native_motion()` in `weldplan/native_admission.py`, and it runs before any dense resampling. A plan gets a `.rdt` only if all of these hold:

1. The connecting-move stage did not fail. Otherwise: `motion_join_failed`.
2. `seams` and `connecting_trajectories` are lists of objects, and every seam has a non-empty, unique `id`. Otherwise: `motion_result_invalid`.
3. Every seam was crossed by the search, with no error. Otherwise: `seam_not_planned`, with where the search stopped and why (poses outside joint travel, rejected by sphere screening, unreachable by inverse kinematics, or with no allowed step from the previous sample).
4. Every seam has a continuous trajectory. Otherwise: `motion_not_verified`.
5. Every seam trajectory's verdict is `PASS`, with `collisions`, `penetrating` and `limit_violations` all exactly 0 and `unverifiable` empty. Its `limit_violations` and `limit_unverifiable` lists are empty. Otherwise: `motion_not_verified`, with the measured numbers and up to the first few findings (which bodies, which joint, how far).
6. Every seam's tracking certificate is `PASS`, with no segment out of tolerance, nothing undecided or unverifiable, and a finite bound between 0 and the declared tolerance. Otherwise: `motion_not_verified`.
7. Every connecting move is an `approach`, `transit` or `retract` whose verdict is `PASS`, with `collisions`, `penetrating` and `limit_violations` all 0 and `unverifiable` empty. Otherwise: `motion_result_invalid` or `motion_not_verified`.

A separate candidate-only mode, used for saving authored welds that have not been planned, refuses any continuous motion with `candidate_only_requires_m4`, so it cannot be used to get around a failed report.

If you turn verification off (`verify=false`), the trajectories carry no verdict, admission refuses them, and no `.rdt` is written. The OLP server always asks for verification.

Admission reads the planner's own reports. It does not authenticate a result document, and it does not re-verify the resampled `.rdt`; see below.

## What is not verified

Be precise about the limits of this check:

- **Motion outside planned programs.** Jog, joint and Cartesian moves, "All joints to 0°", Home, go-home, **Go to plan start**, and anything sent through `rtctl`, the SDKs or the dense trajectory daemon never pass through the verifier. They rely on rt-core's limit and readiness checks only. See the [motion start gate](https://advancedmetalresearch.com/docs/get-started/safety-model#motion-start-gate).
- **Physics.** The verifier is a geometric certificate, not a simulation. It does not model torques, payload, following error, drive behaviour or jerk.
- **Anything not in the model.** People, cables, clamps you did not enter as fixtures, a part that differs from its CAD file, or a part placed somewhere other than its placement says. The result is only as good as the cell meshes, the robot description, the cell calibration and the placement.
- **The M4 screen.** The seam search checks sampled poses against spheres. Its findings are reported as screening results, "not a continuous mesh collision certificate". Only the continuous M5 and M6 output is certified.
- **The exported representation.** The `.rdt` is a resampling of the verified curves on a 0.01 s grid. The exporter slows the start and end of each weld pass with a 0.25 s ramp and adds a 3 s stationary dwell before and after each pass. Positions stay on the certified path, but the verifier does not re-judge the resampled file. The encoder checks its own rules instead: segments start and end at rest, consecutive segments meet within 0.001 rad, no sample-to-sample step is larger than the velocity ceiling allows (with 25 % interpolation slack), and no axis exceeds the cell's per-axis velocity ceiling (`qd_limit_exceeded`). See the [`.rdt` format](/docs/reference/rdt-format).
- **Plan speed.** `speed_scale` (1 % to 100 %) slows the whole plan after planning. The path, and so the clearance and tracking certificates, are unchanged, and the original velocity and acceleration checks stay conservative for any scale up to 1.
- **The weld.** Nothing checks bead quality, arc behaviour or process parameters. Torch outputs are refused everywhere, so no program can switch a torch on. See [Process outputs are refused](https://advancedmetalresearch.com/docs/get-started/safety-model#process-outputs).

You can replay the exact `.rdt` bytes before Load, in the 3D viewer or on the local simulator. That replay is an operator step. The software does not require it.

## The plan result

The planner answers with a result document, schema `amr-weld-planner-v1.motion-plan-result.v1`. Angles are in degrees and lengths in mm on the wire. The document is the evidence for a plan:

| Block | What it records |
|---|---|
| `request` | The SHA-256 of the exact `.weldplan` bytes, the request, program and cell ids, the cell descriptor summary, the source STEP and its digest, every member's digest, and the placement |
| `producer` | `module` (`amr-weld-planner/v1`) and `source_revision`: the full 40-character Git commit of the planner code, or `null` with `source_revision_state: "unavailable"` when it was not supplied |
| `candidate_search` | How M4 searched: `dynamic_programming_over_discrete_redundancy_lattice`, minimising the sum of absolute joint motion, `exact_within_the_sampled_lattice` |
| `coverage` | What was and was not evaluated; see below |
| `options` | Every planning option actually used, flat, with `verify_margin_mm` in mm |
| `timings_ms` | Wall time per stage, in pipeline order |
| `seams[]` | Per seam: whether it was crossed, the K candidates, and the continuous `trajectory` with its `verdict` and `tracking` reports |
| `connecting_trajectories[]` | The moves, each with `kind`, `from`, `to`, knots, `clearance_mm` and `verdict` |
| `dense` or `dense_error` | The `.rdt` summary, or the reason none was written |

The `coverage` block states the claim limits in the document itself:

| Key | Values |
|---|---|
| `candidate_collision_screening` | `sampled_lattice_nodes` or `not_evaluated` |
| `candidate_limit_checks` | `reported_per_candidate` |
| `weld_trajectory_continuous_verification`, `connecting_trajectory_continuous_verification` | `evaluated_for_all_emitted_trajectories`, `partially_evaluated` or `not_evaluated` |
| `canonical_motion_server` | Always `not_evaluated` |
| `physical_motion` | Always `not_evaluated` |
| `execution_authority` | Always `none` |

A plan result grants no authority to move anything. Authority comes from the rt-control lease when you Load and Play. See [One controller at a time](https://advancedmetalresearch.com/docs/get-started/safety-model#one-controller).

When a plan comes back without a `.rdt`, the planner keeps the request and its full result under `<dense store>/refused/`, named by the request digest, so you can diagnose it without planning again. It keeps the newest 5.

The `.rdt` header carries the plan identity (`plan_id`, `program_id`, `program_digest`, revisions) and the robot description and cell calibration identities the plan was made against. `program_digest` is `sha256:` plus the digest of the `.weldplan` request, and `plan_id` is `<program_id>:<first 12 hex characters of that digest>`. OLP's Load refuses a plan made for another robot description or cell calibration. See [Robot description and coordinate frames](https://advancedmetalresearch.com/docs/concepts/robot-description-and-frames#identity).

## Related pages

- [Run the weld planner](https://advancedmetalresearch.com/docs/guides/run-the-weld-planner)
- [Weld planner HTTP API](https://advancedmetalresearch.com/docs/apis/weld-planner-http)
- [Weld program and `.weldplan` container](/docs/reference/weld-program-format)
- [Motion paths and planning](https://advancedmetalresearch.com/docs/concepts/motion-and-planning)
- [Error codes](https://advancedmetalresearch.com/docs/reference/error-codes)

## Sources

Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS):

- `weld_planner/v1/python/weldplan/native_admission.py:1-5,137-250`
- `weld_planner/v1/python/weldplan/dense_joint_trajectory.py:90-110,508-509,655-797`
- `weld_planner/v1/python/weld_motion_planner/verifier/verify.py:1-127,460-606,844-1015,1640-1687,1877-2060`
- `weld_planner/v1/python/weld_motion_planner/verifier/model.py:20-39`
- `weld_planner/v1/python/weld_motion_planner/robot_cell/collision/model.py:1-24`
- `weld_planner/v1/python/weld_motion_planner/planner/planner_main.py:1-27,64-96,160-180`
- `weld_planner/v1/python/weld_motion_planner/planner/dp_seam_search/dp_seam_search_main.py:1-22`
- `weld_planner/v1/python/weld_motion_planner/planner/trajectory_optimization/options.py:21-40`
- `weld_planner/v1/python/weld_motion_planner/planner/trajectory_optimization/engine/weld_trajopt.py:245-294,494-498`
- `weld_planner/v1/python/weld_motion_planner/planner/trajectory_optimization/engine/taught.py:1-60`
- `weld_planner/v1/python/weld_motion_planner/planner/trajectory_optimization/engine/freespace.py:2435-2464`
- `weld_planner/v1/python/weld_motion_planner/planner/trajectory_optimization/engine/control_solver.py:94,175-178`
- `weld_planner/v1/python/weld_motion_planner/io/native_result.py:17-164`
- `weld_planner/v1/python/weld_motion_planner/io/dense_output.py:1-139`
- `weld_planner/v1/python/weld_motion_planner/server/motion_planner_server.py:78-82,137-179,353-377`
- `weld_planner/v1/data/dense_joint_trajectory.params.json`
- `offline-programming/v1/weld_plan.go:424-438`
- `offline-programming/v1/internal/denseexec/session.go:236-258`
