> ## Documentation Index
> Fetch the complete documentation index at: https://docs.altnautica.com/llms.txt
> Use this file to discover all available pages before exploring further.

# World Model and Compute Offload

> The drone selects pose-tagged keyframes; a compute node reconstructs. Capture sessions, the compute job API, and perception offload for an NPU-less drone.

# World Model and Compute Offload

World-model capture is split in two on purpose. The drone does the light half:
it selects pose-tagged keyframes from its camera stream and publishes the
keyframe, pose and capture-state streams. A separate **compute node**
reconstructs the 3D model from them.

The drone does no reconstruction and no training. It never needs to.

| Half                    | Service        | Profile                  | Off by default                                             |
| ----------------------- | -------------- | ------------------------ | ---------------------------------------------------------- |
| Capture                 | `ados-atlas`   | `drone`                  | Yes, gated on `atlas.enabled`                              |
| Reconstruct and offload | `ados-compute` | `workstation`, `compute` | Yes, gated on `atlas.enabled` for the world-model receiver |

Both units self-gate: with `atlas.enabled` false, `ados-atlas` exits cleanly and
the unit is a clean no-op, so an un-opted node is byte-unchanged.

## Capture on the drone

`ados-atlas` subscribes to the vision frame ring, decides which frames are worth
a keyframe, and publishes the keyframe plus pose plus capture-state streams.

Three pieces make up the capture core:

* **The rig**: one camera up to an all-sides set. The camera count is
  configurable from 1 to N and drives one flow at any count.
* **The keyframe selector**: a per-camera gate that fires on pose delta or
  elapsed time.
* **The capture session**: a state machine that takes pose-tagged frames, always
  feeds the roughly 10 Hz pose stream, and emits a keyframe envelope whenever a
  camera's selector fires.

### Configuration

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
atlas:
  enabled: false
  capture_profile: "freeform"   # orbit | lawnmower | freeform | inspection
  reconstruct_steps: 30000
  pose_tier: "auto"             # auto | local | offload | hybrid
  hfov_deg: 70.0
  socket_dir: "/run/ados"
  cameras:
    - id: "main"
      role: "primary"           # primary | aux | down | left | right | back | up
      enabled: true
      reconstruct: true
  selection:
    min_translation_m: 0.5
    min_rotation_rad: 0.26      # about 15 degrees
    max_interval_ms: 2000
    max_keyframes: 0            # 0 = unlimited
```

| Key                           | Default    | Meaning                                                                                                                                                                                         |
| ----------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled`                     | `false`    | The one switch. Off means no capture, no compute services, no perception offload.                                                                                                               |
| `capture_profile`             | `freeform` | The flight pattern the capture assumes. A value outside the four is rejected at the write boundary rather than silently disabling the service.                                                  |
| `reconstruct_steps`           | `30000`    | Default reconstruction detail, in training steps. Read by Mission Control at reconstruct-submit time; the capture service does not consume it.                                                  |
| `pose_tier`                   | `auto`     | Where pose comes from. `auto` and `local` resolve to the flight controller's own estimate; `offload` and `hybrid` carry through so Mission Control can show an NPU-less drone's offloaded pose. |
| `hfov_deg`                    | `70.0`     | Horizontal field of view, used to derive an uncalibrated pinhole when no `intrinsics` entry exists for a camera.                                                                                |
| `cameras[].role`              | `primary`  | One of `primary`, `aux`, `down`, `left`, `right`, `back`, `up`.                                                                                                                                 |
| `cameras[].reconstruct`       | `true`     | Whether this camera's keyframes feed the reconstruction.                                                                                                                                        |
| `selection.min_translation_m` | `0.5`      | Metres of movement before a keyframe is worth taking.                                                                                                                                           |
| `selection.min_rotation_rad`  | `0.26`     | Radians of rotation before a keyframe is worth taking.                                                                                                                                          |
| `selection.max_interval_ms`   | `2000`     | Take a keyframe anyway after this long.                                                                                                                                                         |
| `selection.max_keyframes`     | `0`        | Session-wide cap. `0` is unlimited.                                                                                                                                                             |
| `intrinsics`                  | empty      | Per-camera calibrated pinhole (`fx`, `fy`, `cx`, `cy`, plus an optional distortion model and parameters).                                                                                       |

Pose source resolution, as Mission Control reads it:

| `pose_tier`     | Reported `pose_source` |
| --------------- | ---------------------- |
| `auto`, `local` | `local_vio`            |
| `offload`       | `offloaded_slam`       |
| `hybrid`        | `hybrid`               |

### Readiness

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -s -H "X-ADOS-Key: $ADOS_KEY" http://192.168.1.50:8080/api/atlas/readiness
```

Returns the drone-local facts plus live session state: `enabled`,
`cameras_configured`, `capture_profile`, `pose_source`, the live `state`,
`capturing`, `session_id` and `service_running`, and the `compute_node_id` the
egress forwarder is actively streaming to.

That last field is freshness-gated. A forwarder handoff not re-written within
15 seconds is treated as absent, so a dead forwarder never leaves a stale
compute node on the readiness surface.

Compute-node reachability is Mission Control's own concern (it already knows its
paired workstation nodes), so it is not re-probed here. Only the node the
forwarder actually resolved is surfaced.

### Write the per-drone config

`PUT /api/atlas/config` has patch semantics: only the keys you send are written,
so toggling `enabled` alone never wipes the cameras or the profile. It writes the
`atlas:` block into `/etc/ados/config.yaml` with a surgical merge that preserves
every other key, then restarts `ados-atlas`.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X PUT http://192.168.1.50:8080/api/atlas/config \
  -H "X-ADOS-Key: $ADOS_KEY" -H "Content-Type: application/json" \
  -d '{"enabled": true, "capture_profile": "orbit"}'
```

| Body field          | Meaning                                                                                                                                                                 |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled`           | The per-drone enable.                                                                                                                                                   |
| `capture_profile`   | One of the four profiles. An out-of-set value is a `400`.                                                                                                               |
| `reconstruct_steps` | Default reconstruction detail.                                                                                                                                          |
| `cameras`           | The camera set, each `{id, role, enabled, reconstruct}`. Written verbatim when supplied; absent leaves the existing cameras untouched. An out-of-set `role` is a `400`. |

Validation happens at the API edge rather than at write time, because an
out-of-set value would fail the whole `atlas:` block's parse and silently default
it to disabled: a status surface that lies.

### Drive a capture session

Four routes, each forwarding to the capture service's control socket and
returning the resulting capture status. An unreachable service is a `503`.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST -H "X-ADOS-Key: $ADOS_KEY" http://192.168.1.50:8080/api/atlas/capture/start
curl -X POST -H "X-ADOS-Key: $ADOS_KEY" http://192.168.1.50:8080/api/atlas/capture/pause
curl -X POST -H "X-ADOS-Key: $ADOS_KEY" http://192.168.1.50:8080/api/atlas/capture/resume
curl -X POST -H "X-ADOS-Key: $ADOS_KEY" http://192.168.1.50:8080/api/atlas/capture/stop
```

`stop` finalizes and bags the session, which is what triggers the compute node to
reconstruct.

Capture states: `idle`, `capturing`, `paused`, `finalizing`, `bagged`.

The config and capture routes are writes, so the LAN edge requires the pairing
key when the node is paired, the same posture as `/api/command`.

## The compute node

`ados-compute` is the Rust core of the `workstation` and `compute` profiles: a
SQLite-backed job store, a queue and scheduler with a worker model, the
reconstructor and perception-offload traits, and the master/slave cluster.

There is always one **master**, the single logical endpoint a drone or Mission
Control pairs with and the scheduler. Extra nodes slave to it and offer their
workers. A lone node is the master.

<Note>
  The real reconstructors and detectors are third-party binaries the workers shell
  out to, behind the reconstructor and detector traits. Mock backends keep the
  whole engine testable with no GPU, no camera and no network, which is what CI
  runs. Which backend binaries you install is your decision, not the agent's.
</Note>

### Job kinds

| Kind                 | What it does                                                            |
| -------------------- | ----------------------------------------------------------------------- |
| `reconstruct`        | Build a world model from a keyframe bag: splat, cloud or mesh.          |
| `perception_offload` | A streaming session: frames in, detections back, for an NPU-less drone. |
| `slam_offload`       | A streaming session: frames in, poses back. Offloaded SLAM.             |

Job lifecycle: `queued`, `running`, then one of `completed`, `failed`,
`cancelled`.

A submission carries `job_id`, `kind`, an optional `dataset_ref` (a bag handle,
or a live session id for a streaming offload) and a `params` object. Status
carries `state`, `progress` in `0.0` to `1.0`, an optional `result_ref` and an
`error` when it failed. The heavy result is delivered out of band; the job
interface carries the small request and status only.

### The job API

The daemon serves one TCP listener. Reach is gated by the pairing posture:
unpaired is open, paired plus on-box is open, paired plus off-box needs
`X-ADOS-Key`. That is what makes binding a non-loopback address safe.

| Method         | Path                            |
| -------------- | ------------------------------- |
| `GET`          | `/api/compute/status`           |
| `POST`         | `/api/compute/datasets`         |
| `GET` / `POST` | `/api/compute/jobs`             |
| `GET`          | `/api/compute/jobs/:id`         |
| `POST`         | `/api/compute/jobs/:id/cancel`  |
| `GET`          | `/api/compute/jobs/:id/outputs` |
| `GET`          | `/api/compute/sessions`         |

A worker claims the next job under the engine lock, releases the lock, then runs
the backend without it, so a reconstruction that takes minutes never blocks the
API. It re-acquires the lock only briefly to record the terminal state. A cancel
that lands during the run wins: the scheduler refuses to overwrite a job that is
no longer running.

### Environment

The daemon reads its configuration from the environment so the install layer can
set it without a config-file dependency.

| Variable                   | Default                     | Meaning                                                                                                                                                        |
| -------------------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ADOS_COMPUTE_DB`          | `/var/ados/compute/jobs.db` | The job store.                                                                                                                                                 |
| `ADOS_COMPUTE_WORK`        | `/var/ados/compute/work`    | Dataset and artifact work root. The keyframe persister writes datasets here, the reconstructor writes artifacts here, and the artifact route serves from here. |
| `ADOS_COMPUTE_BIND`        | `127.0.0.1:8092`            | Bind address. The installer opts a node into serving the LAN by changing this.                                                                                 |
| `ADOS_COMPUTE_PUBLIC_URL`  | derived from the bind       | Base URL Mission Control fetches artifacts from. An artifact URL is `<public_url>/artifacts/<relpath>`.                                                        |
| `ADOS_COMPUTE_NODE_ID`     | `compute-node`              | This node's id.                                                                                                                                                |
| `ADOS_COMPUTE_WORKERS`     | `1`                         | Worker slots.                                                                                                                                                  |
| `ADOS_COMPUTE_RETENTION_S` | `86400`                     | Terminal-job retention, in seconds.                                                                                                                            |
| `ADOS_ATLAS_ENABLED`       | unset                       | Overrides the `atlas.enabled` config gate, which controls whether the world-model event receiver is mounted.                                                   |
| `ADOS_PAIRING_JSON`        | `/etc/ados/pairing.json`    | The pairing file, same override as the rest of the agent.                                                                                                      |
| `ADOS_CONFIG_YAML`         | `/etc/ados/config.yaml`     | Read only for the atlas gate.                                                                                                                                  |

A compute node advertises itself over mDNS with its job port, and a drone-side
caller browses for a `profile=workstation` node.

### Status from Mission Control

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -s -H "X-ADOS-Key: $ADOS_KEY" http://192.168.1.50:8080/api/compute/status
```

The compute daemon writes its cluster and queue status to a heartbeat sidecar
(`/run/ados/compute-heartbeat.json`), rewritten roughly every 5 seconds. The
agent's control front serves it so the Mission Control compute card renders
local-first, fresher than the cloud heartbeat.

An absent, stale (older than 20 seconds) or unreadable sidecar is a `404`: this
node is not a compute profile, or its daemon is not running. It is never a `500`.

The served body includes a host `gpu` block with identity and live utilisation.
Every field in it is optional and honest: an unknown field is `null`, never
fabricated, so on a host where a probe tool is missing the whole block degrades
to all-`null` rather than reporting a guess.

## Perception offload

Separate from the world model, and the reason an NPU-less drone can still run a
detector. Two sides, two config blocks.

Drone side:

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
perception:
  offload:
    enabled: "auto"           # auto | on | off
    compute_node_addr: null   # host:port; null means auto-discover over mDNS
```

`auto` offloads when the board has no NPU and a workstation is reachable on the
LAN. `on` forces it, `off` never offloads.

Workstation side:

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
perception:
  serving:
    enabled: "auto"           # auto | on | off
    detector_model: null      # model id; null means the daemon's default
```

`auto` auto-accepts and serves LAN offload. Both blocks default so a fresh agent
needs no setup: a no-NPU drone and a workstation on one LAN offload hands-free.

## Related

* [Swarm bus](/drone-agent/swarm-bus)
* [Node profiles](/getting-started/node-profiles)
* [Multi-camera](/drone-agent/multi-camera)
* [Vision navigation overview](/drone-agent/vision-nav-overview)
* [Node configuration in Mission Control](/mission-control/node-configuration)
