187 lines
9.9 KiB
Markdown
187 lines
9.9 KiB
Markdown
# ADR 0015: Simulation Polygon as a qualification boundary
|
||
|
||
## Status
|
||
|
||
Accepted for architecture; SIM S0 qualification returned `GO`, the S1B
|
||
real-provider start/health/stop boundary passed on 2026-07-24, and the direct
|
||
read-only UI-0 view is implemented and visually verified against that accepted
|
||
journal. Navigation behavior, safety behavior and real actuator authority are
|
||
not accepted.
|
||
|
||
## Context
|
||
|
||
Mission Core has proven a K1 observation/archive/perception vertical, but the
|
||
perception outputs do not yet have a closed-loop consumer. Adding Gazebo, PX4,
|
||
ROS 2 and Nav2 directly to the web process or modeling every experiment as an
|
||
observation session would blur product ownership, lifecycle, evidence and
|
||
authority.
|
||
|
||
Replay, physics simulation, a digital twin, HIL and real shadow also have
|
||
different causal meaning. A recorded stream cannot react to a hypothetical
|
||
command, while a closed-loop world can. Calling both `SimulationRun` would make
|
||
reports easy to misuse.
|
||
|
||
## Decision
|
||
|
||
1. Polygon is a first-class Mission Core bounded context for autonomy
|
||
qualification and assurance.
|
||
2. Mission Core owns scenarios, qualification-run lifecycle, authority policy,
|
||
canonical contracts, provenance, evidence admission, evaluation and reports.
|
||
3. Gazebo, PX4 SITL, ROS 2, Nav2, Rerun and datasets are replaceable providers.
|
||
4. `QualificationRun` is the umbrella aggregate. Its kinds explicitly separate
|
||
`simulation_closed_loop`, `replay_shadow`,
|
||
`digital_twin_closed_loop`, `controller_in_loop`, `hil` and
|
||
`physical_shadow`.
|
||
5. Simulation Orchestrator is the sole process-lifecycle owner. Gazebo and PX4
|
||
do not run inside the Mission Core web process, and the browser has no direct
|
||
PX4 channel.
|
||
6. Gazebo `/clock` is authoritative in closed-loop simulation. ROS 2 uses
|
||
simulated time and PX4 uXRCE-DDS time synchronization is disabled for this
|
||
profile.
|
||
7. Mission Core uses ENU/FLU. PX4 uses NED/FRD. Exactly one adapter boundary
|
||
converts frames and requires golden tests.
|
||
8. The first command boundary permits only rover speed+steering and
|
||
speed+yaw-rate profiles with sequence, TTL, heartbeat, watchdog and
|
||
stop-on-loss behavior. Direct actuator setpoints are rejected.
|
||
9. Source-of-record, derived artifacts and presentation state remain separate.
|
||
Reset creates a new immutable run/episode.
|
||
10. Ground truth is evaluator-only and is not visible to perception or planning.
|
||
11. SIM S0 targets the reviewed Windows 11 worker through a dedicated
|
||
`MissionCore-Sim` WSL2 Ubuntu 24.04 distribution physically under
|
||
`D:\NDC_MISSIONCORE`.
|
||
12. SIM S0 does not depend on the existing Docker Desktop runtime. Its WSL
|
||
backend is C-backed and currently serves unrelated co-tenants.
|
||
13. S1 uses exact `px4_msgs` directly. The experimental
|
||
`px4-ros2-interface-lib` is not a mandatory S1 dependency.
|
||
14. The read-only direct UI-0 run view consumes proven target S1B persisted
|
||
history through GET-only server routes. It is hidden from navigation and
|
||
cannot mutate a run. Top-level Polygon UI and control remain gated on
|
||
accepted S1 target command/lifecycle behavior.
|
||
15. Real actuator authority requires a new decision and physical safety gate.
|
||
16. S0 processes run inside an ephemeral loopback-only Linux network namespace.
|
||
Micro XRCE-DDS Agent's UDP wildcard bind is acceptable only inside that
|
||
boundary; no S0 listener may be reachable from the worker LAN or Windows
|
||
host network.
|
||
17. Product packaging, simulation-worker placement, the future onboard Edge
|
||
Agent, local-first field evidence and offline synchronization follow
|
||
[ADR 0016](0016-distributed-product-edge-and-worker-topology.md). Native
|
||
Gazebo GUI is worker-local diagnostics; the operator product remains the
|
||
browser Control Station over canonical state.
|
||
|
||
## Consequences
|
||
|
||
- Perception can be measured by downstream decisions without granting it hidden
|
||
behavior authority.
|
||
- Standard planners and future Mission Core planners can be compared on the
|
||
same scenario and metric profiles.
|
||
- Replay cannot be presented as closed-loop safety evidence.
|
||
- Provider upgrades require explicit compatibility and evidence refresh.
|
||
- More up-front domain and artifact work is required before a visual demo.
|
||
- The AI worker must satisfy D-only, resource and coexistence constraints before
|
||
installation is accepted.
|
||
- Simulation and HIL reduce risk but do not certify the real trike.
|
||
|
||
## First implementation
|
||
|
||
The qualification doctor is deliberately read-only:
|
||
|
||
- `simulation/s0/qualification-profile.yaml` is the accepted S0 stack,
|
||
storage, clock, authority, port and process contract.
|
||
- `k1link.simulation.s0` strictly validates that contract and optional
|
||
digest-bound worker evidence.
|
||
- `missioncore-sim s0 doctor` inspects the current host without installing,
|
||
starting or modifying anything.
|
||
- MISSIONCOR-40 tracks factual worker qualification.
|
||
|
||
The first target bootstrap also includes `simulation/s0/worker-smoke.sh`. It
|
||
creates the reviewed loopback-only namespace, starts only the pinned stock-rover
|
||
providers, observes ROS 2 vehicle status and Gazebo clock, requests PX4 shutdown,
|
||
checks for owned-process residue and writes D-only factual artifacts. It never
|
||
arms the vehicle or sends an actuator command.
|
||
|
||
`simulation/s0/gazebo_time_control.py` owns the S0 pause/single-step/1×/2× RTF
|
||
and resource acceptance probe. `simulation/s0/build_evidence_manifest.py`
|
||
admits only exact-profile, D-confined, passing 1×/2× runs into the digest-bound
|
||
evidence generation. These scripts are qualification tooling; the S1
|
||
Simulation Orchestrator remains the future product lifecycle owner.
|
||
|
||
The doctor reports `INCOMPLETE` until it is running on the reviewed target with
|
||
resolved/accepted pins and all required evidence. On the accepted profile
|
||
generation and nine-claim confined evidence set it returned `GO`; it cannot
|
||
infer that result from repository state alone.
|
||
|
||
## S1B target lifecycle
|
||
|
||
Exact generation `6cb1495` is the first product-owned real-provider acceptance.
|
||
`SimulationApplicationService` persisted the lifecycle while the worker adapter
|
||
created a fresh loopback-only namespace and `PosixProcessSupervisor` owned two
|
||
dedicated PGIDs: Micro XRCE-DDS Agent and the combined PX4/Gazebo stock
|
||
Ackermann launch. The run became `running` only after the exact world/model,
|
||
PX4 startup and DDS-writer markers existed. No arm, setpoint or other PX4 shell
|
||
command was sent. Stop produced `completed`, eight events, zero commands and no
|
||
process residue.
|
||
|
||
UI-0 now implements that decision. `QualificationRunStore(read_only=True)`
|
||
opens an existing repository without creating or chmodding it and rejects every
|
||
mutating method. The web process exposes only run catalog/detail GET routes,
|
||
and the hidden Control Station workspace opens only from
|
||
`?workspace=polygon-run[&run=<id>]`. Browser QA loaded the accepted
|
||
`s1b-6cb1495-20260724t1535z` journal. This does not widen command authority or
|
||
accept pause/step/reset, canonical telemetry, frame conversion,
|
||
watchdog/failsafe behavior, navigation or physical control.
|
||
|
||
## S1A implementation
|
||
|
||
`k1link.simulation.contracts` now defines the first product-owned
|
||
`QualificationRun`, provider/authority/artifact provenance, canonical
|
||
Ackermann and Differential rover setpoints, and qualification events.
|
||
`k1link.simulation.run_store` persists them as an immutable manifest plus
|
||
one-file-per-sequence append-only journals. Exclusive creation and file/directory
|
||
fsync prevent partially acknowledged records; loading rejects gaps, identity
|
||
drift, non-regular records and illegal lifecycle history.
|
||
|
||
The aggregate derives its current state by replay. Terminal runs cannot accept
|
||
new events, commands or artifacts. A restart never silently resumes an active
|
||
run: reconciliation terminates it as `failed` with
|
||
`orchestrator-recovery-interrupted`. Reset requires a terminal parent and
|
||
creates a new linked run/episode. Commands are admitted only for a running
|
||
closed-loop virtual run with the current authority generation and bounded
|
||
simulation-time TTL. Replay/shadow commands, direct actuator authority and
|
||
ungated HIL/controller-in-loop admission fail closed.
|
||
|
||
This increment proves the local lifecycle/repository contract. It does not yet
|
||
own provider processes, transmit a PX4 setpoint, execute heartbeat/watchdog
|
||
failsafe behavior, or qualify navigation/safety behavior. The target
|
||
orchestrator must place the caller-supplied repository root under the accepted
|
||
D-only worker boundary.
|
||
|
||
## S1B ownership foundation
|
||
|
||
`k1link.simulation.orchestrator` now owns the application lifecycle through a
|
||
transport-neutral worker port. Start and stop requests are persisted with
|
||
idempotency identity, one active run owns a worker at a time, and
|
||
pause/resume/step/reset cannot bypass run-state validation. Provider start
|
||
failure, S0 profile drift, stop residue and restart interruption all become
|
||
explicit run events and terminal failures.
|
||
|
||
`k1link.simulation.process_supervisor` provides the worker-local POSIX ownership
|
||
primitive: shell-free argv, dedicated provider PGIDs, run-scoped logs, atomic
|
||
process registry, partial-start rollback and ascending shutdown order matching
|
||
the accepted S0 dependency graph. `k1link.simulation.worker` binds that
|
||
primitive to the exact accepted S0 profile digest and canonical D-only S1 run
|
||
artifact and process-runtime layouts.
|
||
|
||
The later exact generation `6cb1495` supplied the target provider
|
||
specifications, loopback namespace and live readiness probes and passed the
|
||
real-provider start/health/stop boundary described above. Restart-safe
|
||
PID/start-token reconciliation after orchestrator loss and Gazebo
|
||
world-control/PX4 command adapters remain open. The worker port deliberately
|
||
fixes ownership semantics without making SSH or the browser a provider
|
||
transport.
|
||
|
||
Commit `630d1ae` subsequently passed a D-only `MissionCore-Sim` bootstrap:
|
||
the exact Git archive SHA was verified, both S1 test modules returned
|
||
`20 passed`, and the test process-residue record was empty. This accepts target
|
||
executability of the foundation only; the later `6cb1495` evidence, not that
|
||
bootstrap, closes the real-provider lifecycle boundary.
|