Repository navigation
Live performance groups: relay → Mirror → Python sidecar #245
Copy link
Copy link
Closed
Labels
Description
Summary
Show a pilot's Performance group while a Flight is under way. The relay's Mirror (#247, the groundwork for this issue) copies the iMotions stream to a Python sidecar, which scores the pilot against Targets and places them in the nearest group. The goal is to run this on a live X-Plane flight. Related: #223.
Design: docs/adr/0004 (why a Python sidecar and not Rust in the relay). Relay side: docs/adr/0003 (the Mirror). Implemented on LiveClustering.
How it works
- Our own brief. The client's Instructions and Targets are not available, so for flights we fly ourselves we are the instructor. A Brief (
inference/briefs/*.csv) lists the instructions in order. The first sets all four Flight dynamics factors; after that a blank target carries over, as "turn right heading 360" keeps the altitude. Headings are magnetic, as the X-Plane heading indicator and the plugin show them. Decide the heading reference: the plugin sends magnetic, the client's routes use true #236 stays open for the client's routes, which use true heading. - Marking instructions. The operator presses Enter in the sidecar console as each instruction is spoken. The sidecar records the time and switches targets.
- Settle window. The targets an instruction changes are not scored for 30 s (
--settle), so the turn or level-off doesn't count; Synthetic pilots fly only steady holds. Targets it leaves alone, or restates at the same value, are scored throughout, so altitude lost in a turn counts. - Score. The existing Deviation features (
stdand|bias|per factor, docs/adr/0002), computed by the samecompute_featuresas the offline pipeline. No new score, and no ground-truth tier. - Groups. At start-up the sidecar fits k = 3 on 15 synthetic pilots (seed 0). The live pilot is assigned to the nearest group without refitting, so group numbers don't change during the flight. No group is named until every factor has 60 s scored (
--min-scored). - Output. The console prints a status line every 5 s. Not shown to the pilot, and nothing goes back to the relay yet.
- Saved flight.
data/live_flight_data.csvholds the logger's columns plustarget_*, anddata/live_flight_data_instructions.csvholds each instruction with its time and the targets in force. Both are appended to, like the logger's session file. Runningcompute_featureson the CSV gives each flight's row the features the console showed; per-pilot features average a pilot's flights.
The synthetic cohort makes the group a demonstration of the pipeline, not a measurement of the pilot. Present it that way.
Changes
inference/pilot_inference/performance/live_groups.py: the sidecar CLI. It listens on 5001, the port the relay's mirror box suggests. The socket is read on its own thread so the Mirror never backs up.inference/pilot_inference/performance/live.py: brief loading and per-flight scoring. Each sample is judged by the instruction in force when it arrived.inference/pilot_inference/performance/clustering.py:fit_reference()/CohortGroups.assign().fit()now wrapsfit_reference(), so groups are still formed one way.inference/pilot_inference/capture/replay.py: sends a telemetry CSV in the relay's packet format, for testing without X-Plane.--brief-outwrites a synthetic flight's targets as a brief.inference/briefs/example_level_holds.csv: four level holds at 3000 ft and 100 kts.GLOSSARY.md(Instruction redefined; Brief, Settle window and Reference cohort added),docs/adr/0004,docs/telemetry_schema.md(briefed targets),inference/README.md("Live groups"), and.gitignorefor the live-flight CSVs.- No relay changes: the Mirror already does the forwarding.
Acceptance criteria
- A replayed synthetic pilot from outside the cohort flows through the sidecar CLI and is placed in a group consistent with its hidden tier. Checked by hand with the README's replay demo (a tier-3 pilot lands in the group holding 4 of the cohort's 5 tier-3 pilots); the tests cover the pieces, not this whole run.
- The output says provisional until every factor has the minimum scored, and names the factors still settling. A named group stays shown while a later instruction's factors settle, since the others are still scored.
- Group numbers do not change during a flight (fit once, assign).
- Each flight's row of offline features from the saved CSV matches the live features, also when the file already holds earlier flights.
- The socket is read on a thread separate from scoring and CSV writes, so the Mirror isn't held up (docs/adr/0003).
- A port already held by
data_loggerstops the sidecar with a clear message before anything is written. On Windows it can no longer silently share the port. -
uv run pytestpasses (113 tests). - Lab run: X-Plane → relay →
Connect mirror→ sidecar on a real flight with our brief, while iMotions records. Hardware-bound; can't be verified by an agent.
Lab checklist
- Write the brief in magnetic headings; the first instruction sets all four targets.
- Start
live_groupswith--briefand--pilot-idbefore pressingConnect mirror. - Stop
data_loggerfirst: the sidecar replaces it for this flight. If the port is still taken, the sidecar says so and exits. - Keep the plugin's "Positional Flight Data" window open the whole flight. On Windows and Linux the stream stops when it closes.
- If the sidecar restarts, press
Connect mirroragain; the Mirror does not reconnect by itself.
Out of scope / follow-ups
- Estimating Targets for the client's Prepar3D TSVs (scenario headings 190/40 to 3100 ft, approaches 220/0), without instruction timestamps. Next issue.
- Real reference cohort. Swap the synthetic cohort for real pilots once they have targets. Needs at least 6 (
candidate_ks). - Return channel. Show the group in the relay GUI.
- Ask the client for the scenario briefing, the 7-phase Segment boundaries and per-pilot flight hours. Don't block on them over fall break.
- Pilot metadata stays out of the clustering (docs/adr/0001).
Reactions are currently unavailable
Activity
Phlabry commentedon Oct 23, 2026
The relay side of this is in #247. The relay can now hold a second destination alongside iMotions, so a Python process reads the same packets while iMotions keeps recording. Previously a flight could feed one or the other.
How to use it:
Connect TCPto iMotions as usual, start your listener, then enter its address in the second box and pressConnect Python. Both get identical bytes. If the Python side is missing or stalls it gets dropped and the iMotions stream is unaffected, so a crashed script cannot cost a flight.docs/adr/0003covers why it hangs off the primary connection and what would justify changing that.This does not close the issue. The clustering loop is still yours, and the real blocker is unchanged: with no targets captured anywhere (#213) there is no deviation to compute, so this can only run against synthetic pilots for now.
Issue #245 groundwork: mirror the relay's packets to a second destina…
[-]Live performance groups: update clustering as telemetry arrives through the relay[/-][+]Live performance groups: relay → Mirror → Python sidecar[/+]Issue #245: harden the live sidecar after review
Issue #245: carry targets over and settle only what an instruction ch…
Issue #245: address review of the live sidecar
Issue #245: address Copilot review on PR #249
Issue #245: note where the live sidecar runs and how it stops