Can a robot arm refurbish a water purifier? This repo answers that in simulation, before we spend anything on hardware.
The current build (v5) works on the whole purifier, modelled on DrinkPrime's own process documents — 13 files in total, 10 stage work instructions plus 3 QC flow-chart pages — which landed on 2026-08-04. A used unit is placed on a rotary indexer and the arm works around it in three stations:
- TRIAGE — a fixed camera grades the unit Easy / Medium / Hard from its pixels. 97.5% on 120 fresh units. Missed-hard is 0.0% — no unit needing HARD-class work was ever graded below hard, and dents have no remedy in any of their ten stages, so that is the error with no recovery. False-easy is 2.5%: two MEDIUM units were graded easy and would have skipped the scratch-removal pass.
- CLEAN — the arm picks a wiping pad off a tool changer and rasters all five exposed faces. 100% coverage on held-out units, 0 spots missed of 129, and every contact sample in the force band (peak 23.9 N, inside the 45 N the pad model is validated to). No cream, detergent or chemistry is simulated — what is verified is that the soil the camera could see is gone.
- MEMBRANE ASSEMBLY — Stage 10, their real named station: the threaded cap is driven multi-turn until it stalls on torque (median 9.40 turns at 6.00 N·m), then the assembly is pressurised and dipped for a bubble check. 10/10 honest passes, 10/10 sabotaged units caught, 0 false passes. Their cap is threaded and spanner-tightened, not our v2/v3 bayonet, and the bowl/cap/O-ring geometry is a placeholder until CAD arrives.
Those three are wired together by a routing step that sends each unit down the route its grade selects. On 28 held-out units, and these are four different questions:
| declared-trace conformance | 28/28 — the cell emitted the trace its own prediction selected |
| right route (grade == truth) | 26/28 — capped by triage |
| end-to-end (software) | 26/28 — right route, and every step its route defines succeeded |
| fully backed | 22/28 — …and no claimed action that nothing implements |
Read the last row as the honest headline. The 4-unit gap is the HARD route: it stops at a human scrap-approval gate, and nothing physically delivers the unit there — it is a software decision. Their chart forks HARD on rectification cost, we have no cost model, so the cell refuses to make that call. false-easy is 0/28.
Before that (v4) the same ideas ran on a single cartridge: extract it, grade it from an image, and clean / lightly-wash / replace accordingly. Every route completes 100% of the time with a 0.0% false-clean rate. That loop still runs, and it is still worth showing for one specific reason: v4 dips the cartridge in a solution tank, which is closer to the funding bar's wording than v5's dry cabinet wipe. See the videos below.
New here? Read HOW_IT_WORKS.md. It explains the whole thing in plain English with no robotics background assumed. This README is the summary and the instructions for running it.
media/v7_clean_stroke_realtime.mp4 — one unit end to end at true 1× (2:34.9) — one continuous take, no cuts, every frame captioned in plain English. Full-coverage continuous-stroke cleaning: 100% coverage and 0 spots missed of 273 on 16 fresh units, sabotage caught 10/10 with 0 false verifies.
The older
media/v6_cell_realtime.mp4(2:33) films the targeted cleaning mode, which was WITHDRAWN on 2026-08-10 — under the arm's published speed limits it left residue on 1 of 24 units. That 2:33 figure is retracted; seeHANDOVER.md→ RETRACTIONS.
media/v5_cell_realtime.mp4 — how long a unit actually
takes. One complete MEDIUM cycle at true 1×, no speed-up — the clip's runtime is the
cycle time: 3 min 51 s (clean 186.8 s, membrane + dip 44.1 s, triage/verify cost no
motion time). Rebuild with v5_video.py --realtime.
media/v5_cell_reel.mp4 — the sped-up overview (2:24).
All three routes end to end at a labelled 1.5× (slow enough to dissect), then a stats
card, then a card listing everything that isn't real. Read
media/REEL_WALKTHROUGH.md alongside it — chapter times, what
happens at each station, and which parts the arm actually performs (it is two of them). Each clip is a unit from the published eval set — seeds
1700 / 1704 / 1702 are rows in stats_v5_route.json, so every number on screen can be
checked against the committed artifact. Rebuild with v5_video.py --reel.
media/v5_cell_medium.mp4 — the MEDIUM route alone, at a labelled 4× — TRIAGE → CLEAN → VERIFY → MEMBRANE → DIP — with live workpiece metrics.
media/v5_cell_hard.mp4 — a HARD unit going to the scrap-approval gate, which is where the cell deliberately stops.
media/refurb_cell_v4.mp4 — the v4 cartridge loop, still worth showing because it is the one that dips a part in solution. All three routes back to back with the live camera verdict on screen (~81 s). Trial footage runs at a clearly-labelled 1.8× so three real cycles fit; the stats cards are not sped up.
media/refurb_cell_v4_full.mp4 — the same thing uncompressed and unedited (2:16), for anyone who wants to see true cycle times.
media/refurb_cell_v3.mp4 — the earlier swap-only cycle. media/cartridge_cell_v2.mp4 — insertion alone.
Every station carries its own caveat printed on the frame, because a video is where overstatement is easiest and hardest to catch: the triage class names are DrinkPrime's but the boundaries are ours; no detergent or chemistry is simulated; the dip's bubbles are modelled from torque, not a fluid simulation; the membrane geometry is a placeholder. Regenerate with
v5_video.py --render SEED.
| v5 TRIAGE — grade a whole unit | 97.5% on 120 fresh units; missed-hard 0.0%, false-easy 2.5% |
| WITHDRAWN 2026-08-10 — under the arm's published speed limits it left residue on 1 of 24 units and failed verification on one, against a published claim of 0/24. Its 2:33 cycle time is retracted. | |
| V7 CLEAN — continuous stroke (shipping) | 100% coverage on 16 fresh units, 0 missed of 273, peak 22.6 N, sabotage 10/10, 0 false verifies; clean 108.1 s, full cycle 2:34.9 |
| V10 (2026-08-19) — the POWERED BUFF | the plant's own polish tool (drill + foam disc, on film) modelled on the tool changer with an honest motor. Reaction torque through the changer: 0.29 med / 0.47 peak N·m — the sourcing number. 16/16 at 100%, 0 missed of 332; spin rate 10 rad/s set by a sweep that found a solver cliff at 15 (90.6 N). Cannot show polishing quality (removal is contact-binary) and says so. Demo: media/v10_buff_realtime.mp4 (2:32.2) |
| V9 (2026-08-13) — inline verify, fast spindle, THREE CLOTHS | verify is graded during dwells the clean cycle already owns (battery row-identical to the driven pass; fresh 10/10); the spindle's "dead lever" was a silent ctrlrange clamp, now a 20 rad/s purchased spec (battery 10/10); Stage 3's full wash/rinse/dry runs on three pads and three posts (16/16 at 100%, dry pad asserted never-charged). BASE cycle 2:28.6; FULL STAGE 3 6:20.7 (paired, seed 1704). The leak check's blind spot is now measured: 10/10 damaged O-rings pass at full torque — fix is a pressure-decay sensor |
| V8 SOLUTION station — charged pad (opt-in) | the arm dips its pad in detergent solution before cleaning and re-dips when the charge runs low, per DrinkPrime Stage 3. 100% coverage on 16 fresh units, 0 missed of 252, peak 22.6 N, 1 dip/unit, 0.00 s of dry-pad contact; clean 114.4 s (dip = 6.34 s/unit); paired render on one seed: 2:36.1 dry → 2:42.6 with the dip |
| v5 CLEAN — point raster (baseline) | 100% coverage held out (8/8), 0 missed of 129; force 100% in band, peak 23.9 N vs a 45 N validity ceiling; 180.9 s/unit |
| v5 VERIFY — prove the check can fail | honest 10/10 verified with zero residue, 0/10 false verifies, sabotage caught 10/10 |
| v5 MEMBRANE — Stage 10 | arm really inserts the element 10/10 (aligned 0.30 mm median vs 1.5 mm clearance); cap stalls on torque, median 9.40 turns @ 6.00 N·m; 10/10 honest pass, 10/10 sabotage caught, 0 false pass. The cap is presented by a fixture, not fitted by the robot |
| v5 ROUTING — grade picks the route | route-plan compliance 28/28, right-route 26/28, end-to-end 26/28, false-easy 0/28 |
| Refurb decision loop (v4) | routing 100%, end-to-end 86.7%, false-clean 0.0% (60 held-out) |
| Refurb swap cycle (v3) | 100% (125/125 full cycles) |
| Insertion cycle (v2) | 100% (200/200 trials) |
| Insertion was previously | 88% on its own test seeds, 72% on unseen ones |
| Seating accuracy | 1.7 mm (insert) / 2.0 mm (swap) |
| Cycle time (simulated, not a takt time) | ~23 s insert, ~33 s full swap, ~294 s to clean one unit |
| Physics engine | MuJoCo 3.11.0, pinned |
| Robot | UFACTORY xArm7 (a real, purchasable ~$11K arm) |
| Status | Pre-hardware. Simulation only. No robot has been bought. |
Full breakdowns: V5_PLAN.md (current work — v5 plan and every v5 result), RESULTS_V4.md (the decision loop), RESULTS_V3.md (swap cycle) and RESULTS.md (insertion). CELL_SPEC.md is what you would build a real cell from.
Caveats that must travel with the numbers above. The class names Easy / Medium / Hard are DrinkPrime's, and (verbal, 2026-08-12) so are the outcome definitions — EASY device not changed / MEDIUM some parts changed / HARD entire device changed. The visual boundaries the camera applies are still ours, because no document defines what an operator sees — that remains the top open question, now narrowed to its visual half. v5 CLEAN is a dry pad wipe of the cabinet exterior; v8 charges the pad from a solution tray — but only the LOGISTICS of solution are modelled, never the chemistry. There is no fluid simulation anywhere in this repo, and solution does not make soil easier to remove here, because removal is binary on contact. What v8 measures is what it costs to use solution correctly: carrying it, spending it, and going back for more. Stage 3's follow-on rinse and dry passes need two more pads on two more tool posts and are not built. The membrane bowl, cap and O-ring are placeholder geometry pending CAD. The dip's bubbles are modelled from cap torque, not a fluid simulation — what stays real is that a camera has to actually see them to fail the part. And end-to-end is capped by triage: both routing misses are triage errors propagating, and 26/28 is statistically consistent with the grader's own 97.5%.
Scope note. The v2/v3/v4 cartridge work is a generic manipulation primitive. Their corpus contains no teardown or disassembly instruction at all, so the extraction motion those versions are built on has no documented counterpart in DrinkPrime's process. v5 is the version modelled on their actual stages.
Needs Python 3.10 or newer. Three commands:
git clone git@github.com:PranavMulakala/WaterFilterRobo.git
cd WaterFilterRobo && ./setup.sh
cd sim_poc && ../venv/bin/python sim_poc4.py --eval 60 --seed0 200That runs 60 complete randomized refurb cycles on held-out seeds and prints inspection
accuracy, the false-clean rate, routing compliance and end-to-end success. Takes a few
minutes. sim_poc3.py --eval 25 is the swap cycle alone; sim_poc2.py --eval 25 the
insertion alone.
For the current (v5) stations:
../venv/bin/python v5_triage.py --eval 120 # grade 120 units -> 97.5%
../venv/bin/python v5_clean.py --eval 8 # clean 8 units -> 100% coverage
../venv/bin/python v5_membrane.py --sabotage 10 # Stage 10 + bubble -> 10/10, 0 false pass
../venv/bin/python v5_route.py --eval 28 # full routed cycle -> 26/28 end-to-end
../venv/bin/python v5_video.py --render 1607 # re-render the v5 demo cut (writes no stats)
# V7 continuous stroke (the shipping clean mode) and V8 the solution station:
../venv/bin/python v5_clean.py --eval 16 --stroke --seed0 2120 # 100%, 0/273, 22.6 N
../venv/bin/python v5_clean.py --eval 16 --stroke --solution --seed0 2204 # + charged pad, 0 s dryEach command's default
--seed0is the one that reproduces its published number (triage 700, clean 900, membrane 1400, routing 1700) — run them bare. Note the membrane baseline is the paired--sabotage 10run, which writes 10 honest and 10 deliberately under-tightened units; a plain--eval 10writes a different, smaller result over the same file.
Every eval overwrites the
stats*.jsonit belongs to, including a 2-seed smoke test. Those files are the recorded baselines the project is measured against, and the default seed of each command is the one that reproduces the published number. Change the seed range and you overwrite a baseline with something else. Checkgit diff -- sim_poc/stats*.jsonbefore you finish.
Don't run a bare
python3 -m venv venv. On a stock Macpython3is Python 3.9, which has no MuJoCo build available, and the failure message points at something unrelated.setup.shfinds a suitable interpreter and says so plainly if it can't.
# watch it live in an interactive 3D viewer — drag to orbit
../venv/bin/python -m mujoco.viewer --mjcf ufactory_xarm7/refurb_scene_v5.xml # current
../venv/bin/python -m mujoco.viewer --mjcf ufactory_xarm7/cartridge_scene_v3.xml # the v3 cell
# re-render one swap cycle as a video (output index, seed, speed)
../venv/bin/python sim_poc3.py --render 1 3 1
# rebuild the title/results cards and stitch the demo reel
../venv/bin/python sim_poc3.py --cards # (or sim_poc2.py for the insert-only reel)
../venv/bin/python sim_poc3.py --concat| File | What it is |
|---|---|
| HOW_IT_WORKS.md | Start here. Plain-English explanation, no jargon |
| RESULTS.md | The numbers, the bug that was found, and how it was verified |
| ARCHITECTURE.md | Technical detail: control loop, geometry, phase table |
| PROJECT_CONTEXT.md | Business context — the venture, the people, the economics |
| PITCH_NOTES.md | Customer-facing framing and the open questions for the founder |
| LEARNINGS.md | The robotics concepts, in plain English. Written for us, not for customers |
| PAIN_JOURNAL.md | Every gotcha hit, with time lost. Deliberate product research |
| V5_PLAN.md | Current work — the v5 plan and every v5 result |
| CELL_SPEC.md | What you would build the real cell from — frames, stations, tolerances, tooling |
| CLAUDE.md | Working rules for this repo, and the current state at the top |
| RESULTS_V4.md | The v4 decision loop, and what limited it |
| RESULTS_V3.md | The swap-cycle numbers, and why the untwist slips |
sim_poc/v5_triage.py |
v5 station 1 — grade a whole unit from a fixed camera |
sim_poc/v5_clean.py |
v5 station 2 — plan-then-execute wipe of all five faces, plus the verification + sabotage test |
sim_poc/v5_membrane.py |
v5 station 3 — Stage 10: cap to torque, pressurise, dip, bubble check |
sim_poc/v5_route.py |
The routing loop — the grade selects the route; reports compliance, right-route and end-to-end separately |
sim_poc/v5_video.py |
Renders a full route cycle with per-station on-frame caveats |
sim_poc/make_scene_v5.py |
Generates the v5 scene from a fixed seed. Edit this, never the XML |
sim_poc/v5_reach.py |
Reachability study — can the arm get to every face at all |
sim_poc/v5_check_scene.py, v5_check_spec.py |
Gates: the scene is sane, and CELL_SPEC still matches the model |
sim_poc/dp_process.py |
DrinkPrime's real 10-stage process, as data. No side effects |
docs/drinkprime/ |
Their 13 source PDFs. Read its README first — the QC pages are vector art and must be rendered, not text-extracted |
sim_poc/sim_poc4.py |
v4 refurb loop — inspection CV, routing, cleaning station |
sim_poc/sim_poc3.py |
v3 refurb swap cycle — extends v2 by import, adds extract + discard |
sim_poc/sim_poc2.py |
The insertion system — perception, planning, control, evaluation |
sim_poc/ufactory_xarm7/ |
Robot model; cartridge_scene_v3.xml adds the spent cartridge + bin |
sim_poc/stats*.json |
The recorded baselines. Overwritten by any eval — treat as protected |
setup.sh, requirements.txt |
Environment setup, with versions pinned on purpose |
It does prove the task is mechanically achievable by an off-the-shelf arm, that the strategy is sound, that we understand where the difficulty actually lies, and that we can show a customer a working demo without buying anything first.
It does not prove that real cartridge tolerances match the model, that a real camera localises the part this well, that real friction behaves like simulated friction, or that the cycle time survives a real production line. Sim-to-real transfer reliably costs more than expected.
Honest framing: this de-risks the approach, not the deployment.
Every v5 phase (V5-0 through V5-G) is done. The remaining work is not code:
- Ask the founder what defines Easy / Medium / Hard — open-ended, because no document in their set defines it. Every triage boundary we use is ours wearing their vocabulary.
- Check what the funding bar actually means. It is recorded as image inspection plus "the arm cleans the filter with a solution". v5's CLEAN is a dry wipe of the cabinet exterior — different part, no fluid. v4's cartridge dip is much closer. Nobody has checked which was meant, and it changes what to build next.
- Re-derive the membrane geometry when the CAD lands. The bowl, cap and O-ring are placeholders, so the turns-to-torque figure is a mechanism result, not a part result.
Blocked on DrinkPrime, in priority order: what defines Easy / Medium / Hard (must be asked open-ended — no document in their set defines it, and if their operators grade by feel then camera-only inspection is the wrong architecture); QC Annexures 1–13; whether the final leak test is 45 or 80 PSI (their documents say both, and it is a safety question); CAD for the membrane bowl, cap and O-ring; and the cabinet's outer dimensions and mass.
Done since this list was last written: all five v5 stations (triage, clean, verify, membrane, routing) plus the v5 video; the process documents landed and are transcribed; LeRobot v2.1 export is written (structurally validated, not round-tripped through the real library); and the domain-randomization sweep produced the ±2.5 mm fixture-tolerance envelope in RESULTS_V4.
The founder has indicated $20–30K of funding for robot arms if the refurb process can be credibly simulated, with an explicit bar of image inspection plus a cleaning step — which is why refurb fidelity outranks polish on the insertion cycle. That indication is verbal, not in writing, and hardware purchase stays on hold until it is.