Skip to content
vineigenphasePublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

 
 

Repository files navigation

Skopos

A pre-deployment readiness check for home robots. Scan a room, sample thousands of plausible variants of it, find where a fetch task fails, and get a verdict — before the robot ships to that home.

A home robot should meet your home before it ships to your home.


Quickstart

pip install -r requirements.txt
python -m uvicorn app:app --host 127.0.0.1 --port 8077
open http://127.0.0.1:8077

No API keys, no network, no npm. python selftest.py checks every claim below.

Live 3D room demo

The homepage opens with an orbitable 3D view. Map my room replaces the sample with geometry from your device, then runs a campaign using the objects you placed:

  1. AR floor capture: open the app over HTTPS on a browser/device supporting WebXR immersive AR, hit testing and DOM overlays (for example, a compatible Android phone with Chrome). Grant AR permission, point the green ring at each floor corner in order, and press Place corner. Finish after at least three corners. Tracking estimates the floor outline; this is not automatic mesh reconstruction or furniture detection. WebXR availability is checked on-device.
  2. Entered measurements: if AR is unavailable, enter the rectangular room's width and depth, then press Set floor. An optional local camera preview is a visual reference only; it does not estimate depth.
  3. Enter ceiling height, robot start, mug target, and furniture coordinates. Press Use this room in the live demo. Connected viewers update as well, so a phone can supply the room displayed in another browser.

Sample, manually entered, and AR-estimated rooms have distinct labels. Ceiling height is entered (default 2.5 m); object heights are illustrative. Walls are visual guides and are not collision geometry in the surrogate. Furniture is scored using its circular footprint. The engine is unchanged.

Camera images stay in the capturing browser; only validated geometry crosses the WebSocket. Closing the mapper stops the camera. Camera and AR permissions may be blocked by embedded browsers: open the HTTPS app in a regular browser tab when needed. Desktop webcams and browsers without WebXR cannot perform the AR capture. Physical device accuracy still needs validation in a real room.

This remains a single shared demo: applying a room changes it for everyone connected to this server. Room geometry lives in server memory; recording a demo explicitly persists structured events through the existing recorder. Use Reset to sample to return to the original scene.

Run make test for the engine and room-import tests. make test-js runs the AR-coordinate/geometry tests with Node 18+ (Node is only needed for these tests, not to serve the app).

Architecture in five bullets

  • engine.py is the sampling engine and a fixed dependency — scene graph, surrogate scorer, Monte Carlo campaigns, importance re-weighting and cross-entropy search. Stdlib only. Nothing else in this repo modifies it.
  • A campaign scores on the scene graph, never on pixels. ~20,000 configurations per run through a microsecond surrogate; twelve of them get a world-model call. That ratio is the point.
  • Sample once, re-score many times. A defensive mixture proposal bounds the importance weights, so the same samples can be re-weighted onto any home archetype for zero new generations — with the effective sample size shown next to every estimate.
  • bandit.py is a contextual bandit (LinUCB) over four task strategies, adapting online as the room changes.
  • app.py is FastAPI + one WebSocket; static/index.html and the local room capture/view scripts are vanilla JS, no build step, no external requests. A second page, /internals, shows the machinery itself.
scan ──▶ SceneGraph ──┬──▶ surrogate ──▶ campaign ──▶ readiness + hazards
    [pixels stop here] │                     ├──▶ re-weight ──▶ per-archetype verdict
                       │                     └──▶ elite(12) ──▶ world-model render
                       ├──▶ cross-entropy search ──▶ rare-but-plausible failures
                       └──▶ contextual bandit ──▶ strategy selection

What this is not — please read before judging

  1. Online adaptation is a contextual bandit, not a trained policy. A bandit is the one-step case of reinforcement learning: one decision, one reward, no state transition, no credit assignment over time. Nothing in this repo is trained. selftest.py greps the whole codebase to enforce it.
  2. This is not a digital twin. There is no metric ground truth, no collision mesh, no physics and no SLAM. Positions are approximate metres in a room-local frame, and the failure model is a hand-written geometric and semantic surrogate whose every coefficient is visible in engine.py. A perturbed room is a plausible hypothesis about how a home might differ, not a measurement of one.
  3. The rendered frames are a mock by default. providers/mock.py draws a stylised plan view and is what you see unless you supply credentials. It is labelled MOCK RENDER / not a world model inside the image itself.
  4. The Reactor adapter has never run against the live API. It is written against the public docs at https://docs.reactor.inc, read at build time. Facts from the docs are marked [docs] in the source; every inference is marked TODO(reactor). Its health() says so out loud.
  5. The initial demo room is hand-written, not scanned. DEMO_ROOM in engine.py is six objects a human typed in. Map my room can replace it with AR-estimated floor corners or entered dimensions; objects are manually placed. No vision model runs.
  6. The readiness thresholds are a judgement call, not a result: READY ≥ 85, MARGINAL ≥ 60. readiness = 100 × (1 − P(failure)).
  7. The archetype priors are assumptions, not measurements of real homes.

Anything provisional is named with a placeholder_ prefix. There are currently none, and the self-test fails if one appears unlabelled.

The statistics, and the one rule that matters

We want P(failure) under several home archetypes. Sampling each archetype separately would cost a full campaign each. Instead we sample once from a defensive mixture proposal and re-weight:

P_i(fail) ≈ Σ w_m · 1(fail_m) / Σ w_m ,    w_m = p_i(θ_m) / q(θ_m)

q mixes every target archetype plus one deliberately over-dispersed component, which bounds p_i/q ≤ K and floors the effective sample size at roughly M/K. That is what makes re-scoring statistically honest rather than a trick.

And the rule: an importance-weighted estimate is never displayed when its effective sample size is too small. Below ESS_MIN = 200 the weights have degenerated onto a handful of samples and the number is noise, so the UI greys the row out and shows the ESS instead — and the headline verdict becomes a hatched ESS TOO LOW panel rather than a score.

You can watch this fire. Run the adversarial search, then select adversarial (CEM) in the archetype dropdown: the converged prior sits far from the proposal, ESS collapses to around 50, and the verdict is withheld. Measured, in selftest.py, not asserted.

The engine room — /internals

The demo page shows conclusions. /internals shows the machinery that produced them, on one page, every chart drawn on a raw <canvas> with no plotting library:

panel what it shows
pipeline 1 scene graph → 20,000 configurations → 20,000 surrogate calls → 12 world-model calls. The ratio that justifies the whole design, with live counters
monte carlo the severity of every sampled room as a histogram, with the engine's own FAIL_THRESHOLD drawn on it and the failing mass in red
convergence P(failure) as samples accumulate, with its 95% binomial interval narrowing as 1/√n — precision being bought, and the price visible
perturbation space where the mug actually landed across ~1,200 sampled rooms, red for failed. A continuous space with nothing to enumerate
what drives failure failure rate against each perturbation axis, measured off the campaign's own samples. The lighting panel is the finding: ~100% failure in the darkest rooms against ~23% in the brightest
importance weighting the log-weight distribution per archetype (log-scaled counts, so the tail is visible), ESS bars against the floor, and how much of each estimate rests on its top 50 samples
contextual bandit the UCB decomposition (solid = θ·x, faint = the exploration bonus) and a per-feature heatmap of θᵢ·xᵢ. The entire model is five numbers per arm, and they are on screen
adversarial search the CEM failure rate climbing per iteration, and the proposal's own parameters before and after — light_mean 0.68 → 0.06, discovered, not told

Nothing on that page re-implements the engine. Every number is derived from engine outputs — severities, thetas, weights — because a second copy of the failure model would drift from the first.

What you can do in the UI

control what happens
Run campaign streams 20,000 samples in ten chunks; the readiness estimate visibly settles rather than jumping, then the worst twelve rooms are rendered
archetype dropdown re-weights the existing campaign onto another home — 0 new generations, ESS shown for each
Clear the worst hazard acts on the report: moves the worst attributed object off the robot's path and re-measures
move an object drag x/y; dragging updates the graph and the render, releasing re-measures
Run search cross-entropy search refits the sampler to its own worst outcomes
Record / Replay dumps a run to runs/<id>/ and plays it back from disk with nothing on the network

There is deliberately no lighting slider. engine.apply() overwrites scene.lighting with the sampled theta.lighting, so editing the base scene's lighting would change the render and change nothing about the measurement. Lighting and clutter are sampled perturbation axes, not properties of the scan — you change them by switching archetype.

Measured on the build machine

surrogate throughput ~6,400 samples/s (not the 35,000 quoted for engine.py elsewhere — measure yours)
20,000-sample campaign ~1.2–3.1 s, on a worker thread so the socket never stalls
re-weighting onto another archetype ~460 ms, 0 new generations
cross-entropy search 5 × 400 samples in ~150 ms; failure rate 46% → 100%
rendered frames per campaign 12 of 20,000 (0.06%)

A representative run: the scanned room scores 64 MARGINAL with the glass table causing half the failures. Clear it → 72, and the dark rug becomes the bottleneck. Clear that → 81, and what remains is the mug simply not being there, which no amount of moving furniture fixes.

Privacy

Frames are processed in memory and discarded. Only the scene graph — structured text — crosses a network boundary, and the UI shows the exact payload live next to a counter for frames processed, frames discarded, frames written to disk and a permanent pixels transmitted: 0.

Nothing is written to disk outside SKOPOS_DEBUG_KEEP_FRAMES=1, which defaults off; when it is on, the startup assertion shouts about it and the panel turns red. The assertion is logged at startup:

PRIVACY: frames are processed in memory and discarded; only the SceneGraph (structured text, no pixels) leaves this device.

Configuration

Copy .env.example to .env (gitignored; there are no secrets in the code).

variable default meaning
SKOPOS_PROVIDER mock mock | reactor | runware
SKOPOS_SAMPLES 20000 campaign size
SKOPOS_SEED 20260912 seed; a campaign is reproducible from it
REACTOR_API_KEY — see docs/PROVIDER_SWAP.md
RUNWARE_API_KEY — fallback provider
SKOPOS_DEBUG_KEEP_FRAMES 0 debug only; 1 writes raw frames to disk

Going live: docs/PROVIDER_SWAP.md is a ten-minute checklist.

Licence

MIT.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages