--- name: inspect-simulation description: > Observe a running LunCoSim through its API: read entities, telemetry ports, Modelica or cosimulation variables, time series, and viewport screenshots. Use for questions about live values, motion, scene contents, or visual state. This is the read-only complement to test-via-api, which drives and verifies; use build-usd-scene when the task is authoring. --- # Inspect a running simulation Observe over the HTTP API / MCP — never by polling logs or asking the user. The app must be running with `--api` (default port **4101**; launch per [`test-via-api`](../test-via-api/SKILL.md)). Drive from curl `POST /api/commands`, or the `mcp__lunco__*` tools if wired. For scene, motion, Editor, or viewport inspection, require the visible headful production window and keep it open while reading state. Pair typed queries with `CaptureScreenshot` and inspect the resulting image so the user can see the same state being diagnosed. A headless session is acceptable only for telemetry-only inspection with no visual or user-observation claim; never switch to it silently when a visual check is requested. > **Read the ports, not the log.** A telemetry port snapshot is the authoritative > current value. `tail -f`/`sleep`-polling a log for a number is the anti-pattern. ## The read surface | Tool / query | Answers | |---|---| | `list_entities` (`ListEntities`) | every registered entity → `{api_id, name, type, pos}`. **Start here** — most reads need an `api_id`. | | `query_entity` (`QueryEntity {id}`) | one entity's pose/name/type blob. | | `QueryUsdPrim` | composed USD attributes and the resolved world position for a prim; use it to verify `xformOpOrder`, placement heading and mounted-part dimensions. Add `topology:true` for one scoped visual/collider/material/joint/bounds/projection record. | | `InspectUsdViewport` | the explicit focused USD preview/view handles, document IDs, edit targets, projection generations, and `projection_ready` state; use this to identify the exact editor item visible in a screenshot and wait for a ready preview before editing. | | `InspectUsdInspectionPresets` | persisted view-only USD inspection camera presets; use it to verify the named settings state and the active preset without treating camera presentation as authored USD. | | `read_ports` | **live telemetry.** With `api_id`: that entity's ports `[{name,value,direction,kind}]`. Without: EVERY port-bearing entity (large — pass `name_filter` substring and/or `ports:[…]` to narrow). One-shot. | | `read_port` `{api_id, port}` | a single named port value. | | `watch_ports` `{api_id, …}` | a **time-series** of ports (use when you need change over time, not a single sample). | | `snapshot_variables` (`SnapshotVariables`) | current Modelica variable values (the solver's state). | | `ListTelemetryChannels` / `QueryTelemetryHistory` | retained scalar catalog and history. Keep the returned channel key; an archived channel remains queryable under its captured `api/:` key after its source disappears. | | `cosim_status` | every USD-driven cosim entity end-to-end: `{name, y, vy, netForce, force_y_input, buoyancy, modelica_*}` — verify a **Modelica → physics** chain without logs. | | `rover_status` | rover-specific convenience readout. | | `capture_screenshot` (`CaptureScreenshot`) | raw PNG — save `-o /tmp/x.png`, then Read it. Confirms what numbers can't (did it tip over?). | ## Inspect an authored assembly When the question is about a reusable USD assembly rather than only a spawned entity, use the namespaced Rhai `model_authoring` reads with the exact document id and root path: ```rhai let context = model_authoring::model_context(doc, root, edit_target); let ready = model_authoring::readiness_report(doc, root, edit_target, policy); let graph = model_authoring::port_graph(doc, root, edit_target); ``` `context` is the complete composed tree and `ready` is the explicit topology/physicality/mount/connection/control/runtime preflight. These reads preserve the document generation and complement `InspectUsdViewport` and `LintReport`; they do not mutate or save the document. Use the returned exact paths for selection, reveal, framing, and follow-up typed commands. The [scripting guide](../../docs/scripting-guide.md#model-and-assembly-authoring-human-and-ai) documents the authoring facades and dry-plan boundaries. For generic authoring evidence, use the `authoring_inspection` Rhai library with the same exact document and preview identities: ```rhai let diff = authoring_inspection::candidate_diff(before_doc, after_doc, affected_paths); let groups = authoring_inspection::group_diagnostics(lint_findings); let evidence = authoring_inspection::inspection_snapshot(doc, root, path); let mode = authoring_inspection::inspection_mode(doc, root, path, true); ``` `candidate_diff` is path-scoped and reports consequences plus lint readiness; `group_diagnostics` preserves raw findings; and `inspection_snapshot` keeps visual, collision, joints, frames, materials, provenance, schemas, and connections in one read-only record. Absence is evidence, not permission to invent a bound or provenance. To act on one finding, pass its absolute `subject` to `authoring_inspection::navigate_diagnostic(preview, view, finding)`; it uses the canonical selection owner and exact `FrameUsdPreviewSelection` path. For screenshots, query `InspectUsdViewport` first, require `projection_ready:true`, and correlate the returned `UsdPreviewId` and `UsdPreviewViewId` with the screenshot. View-only camera state is persisted through the `assembly_edit` wrappers `save_inspection_preset`, `apply_inspection_preset`, `delete_inspection_preset`, and `inspection_presets`; verify it with `InspectUsdInspectionPresets` and the viewport's `active_preset`. These presets do not modify USD or simulation state. To perturb-then-observe: `set_input` / `SetPorts {target, writes:[[name,val]], producer_id}` to poke a live input through the next fixed tick; reuse one stable nonzero producer id for the same external caller, then re-read. Use `ReleasePort` / `ReleaseControl` with the same producer id when releasing a live hold to return an input to authored wiring. Apply a safe state with explicit named `SetPorts` values and keep that hold active while it is wanted. Use `possess_vessel` when the target also requires an explicit control claim. ## Recipe 1. `list_entities` → find the `api_id` of the thing you care about (by `name`). 2. `read_ports {api_id, ports:[…]}` (or `read_port`) for the value(s) — filter, don't dump. 3. Need a trend (settling, oscillation, arrival)? `watch_ports` for a series instead of hammering `read_ports`. 4. Modelica in the loop? `snapshot_variables` for solver state, or `cosim_status` for the whole chain. 5. `capture_screenshot` → `/tmp/x.png` → Read it, to confirm the physical picture. If the scene has no authored window camera, the windowed luncosim host may use its explicit standalone presentation policy after finite USD bounds settle; it reports the generated owner in the Camera menu and status history. Headless and recording hosts do not opt into that policy. A transient camera-less projection, or a boundless standalone scene, is a presentation diagnostic rather than a terrain or physics failure. Before interpreting a live read as a finished scene, check `GET /api/ready` and require `ready:true`, `world_hold:false`, and `pending_count:0`. A port may be absent while its Modelica island is still compiling; that is different from a valid zero. For DEM terrain, read the typed `TerrainLodStatus` query as the authoritative geometry stream state. A settled visual terrain requires `wanted == resident` and `pending == 0`; the status-bar text is presentation history and is not a readiness signal. Streamed Lit terrain also waits for its USD material source projection and any required off-thread derived surface/normal product before exposing the initial tile set. During startup, a status entry may intentionally remain live at `resident == wanted` while `pending > 0`: that is render-material publication, not a completed tile bar. Observe the live `terrain-derived` status entry and the typed query rather than treating a historical terrain event as proof that materials are settled. Static USD DEM terrain keeps its `UsdShade` appearance intent on the terrain owner while the generated mesh is assembled. For an interactive USD edit, query `InspectUsdViewport` before describing or changing the visible item, then correlate its explicit view/preview handle with `CaptureScreenshot` and `ListOpenDocuments`. Use the returned document ID, edit target, and projected generation in typed USD commands; do not infer the target from a tab title, file name, or entity name. ### Fixed-panel rover readout For a fixed solar deck, list the rover-root network entity and read its boundary ports plus the panel and battery member outputs. The useful minimum is `solar_power`, `solar_incidence`, panel `power_out`/`generated_current_a`, and battery `terminal_current_a`/`soc_out`. A positive mesh count or a visible `SolarPanel` prim does not prove that current reaches the battery. For a placed rover, pair the live pose with `QueryUsdPrim` on the composed rover prim. Confirm the local forward-axis contract, the effective rotation op and its `xformOpOrder`; do not infer orientation from a screenshot chosen on a symmetry axis. For a selected Editor prim, use `QueryUsdPrim { topology: true }` when the question is why a part is visible, collidable, or not attached as expected. Inspect `topology.parts` for the visual/collider flags, inherited purpose, collision state, per-shape canonical bounds, local/world transforms, source-layer/material/shader bindings, and `topology.joints` for body targets. Treat non-empty `topology.diagnostics` as an authored-data or projection issue; do not fill a null bound with a guessed box. `topology.projection` reports the document/live-stage generation, while `topology.binding` reports the existing visual-sync and physics markers for the selected runtime entity. ## Example (curl) ```bash # what's spawned? curl -s -X POST http://127.0.0.1:4101/api/commands -H 'Content-Type: application/json' \ -d '{"type":"ListEntities"}' # read selected lander ports; use ListPorts to discover exact names curl -s -X POST http://127.0.0.1:4101/api/commands -H 'Content-Type: application/json' \ -d '{"type":"ExecuteCommand","command":"ReadPorts","params":{"api_id":,"port_names":["altitude","descent_rate"]}}' # confirm visually curl -s -X POST http://127.0.0.1:4101/api/commands -H 'Content-Type: application/json' \ -d '{"type":"ExecuteCommand","command":"CaptureScreenshot","params":{}}' -o /tmp/x.png # then Read /tmp/x.png ``` ## Gotchas - **Direction tracker**: inspect the entire coordinate chain together: the world-to-mount direction ports, controller setpoints, measured joint angles, and the rendered boresight. A `locked` or low-error controller output alone can validate the same incorrect frame convention that points the mechanism away from its target. - **`read_ports` without an `api_id` is huge** — always `name_filter` and/or `ports`. - **`api_id` (API-stable) ≠ the rhai `GlobalEntityId`** — get `api_id` from `list_entities`, don't reuse a gid from a script. - **Port not found / empty?** The entity may be pre-compile (Modelica hasn't produced variables yet — `cosim_status` shows nulls until it does), or the name is a USD-path substring you haven't matched. List its ports first with `read_ports {api_id}` (no `ports` filter) to see the real names. - **Wrong port?** The canonical API port is **4101**; set `LUNCO_API_PORT=4101` if the MCP tools miss. - **Don't restart to "get clean state"** — read the running instance; see the ⚠️ in [`test-via-api`](../test-via-api/SKILL.md). - **One-shot vs series:** `read_ports` samples once (call again for fresh values); use `watch_ports` for a time-series — don't sleep-loop `read_ports`. For continuously updated wheel tracks, pair `InspectVehicleTrail` history and publication reads with screenshots after sustained movement past a waypoint. CPU publication alone cannot establish that a resized annotation image reached the material's GPU binding. The render binder owns descriptor-change rebinding; ordinary content uploads must preserve readiness.