Files
isaac/scripts/tune_sweep_standalone.py
dasha_f 0d32f32db0 Сортировочная ячейка Isaac Sim: CV-пайплайн и меши товаров
Замкнутый контур "поток -> CV -> механика": товары идут по конвейеру с шагом 700 мм,
класс определяется стереопайплайном во время движения, пушер и плуг реагируют физически.

Состав:
* control_test/ - ячейка и CV. run_sorting_cv.py + cv_worker.py (два процесса, потому что
  torch внутри Isaac роняет сцену), cell.py (физика лент, плуга, пушера), measure_plane.py
  (замер габаритов), README.md и .memory.md с замерами, проблемами и ловушками
* robozon_sorter/ - модули симуляции, scripts/ - утилиты, scene/ - сцены
* assets/ - меши товаров, плуг, объекты Objaverse

Бейзлайн CV: DEFOM-Stereo vitl, вход 480, iters 24, кроп зоны осмотра, без сегментации.
На потоке 700 мм - классы 8/9, габариты MAE 32.8 мм, 469 мс на товар при такте 700 мс.

Веса моделей (4.5 ГБ) и пропсы конвейера NVIDIA (274 МБ) не включены - источники и
команды скачивания в MODELS.md. Выход прогонов (captures/, runtime/) не включён:
воспроизводится.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 13:07:24 +00:00

152 lines
6.4 KiB
Python

#!/usr/bin/env python3
"""Sweep-rate tuning as a standalone Isaac Sim process.
/home/whatevenif/isaacsim/python.sh scripts/tune_sweep_standalone.py \
--rates 120,200,300,450 --items 4 --headless
Same experiment as `tune_sweep_rate.py`, but it brings up its own `SimulationApp` instead
of being sent into a live Kit through the code-editor socket. That socket stopped returning
results on anything longer than a minute or two - the code kept running (one run wrote its
log in full) but the reply never arrived, so six runs in a row looked like failures. A
standalone process writes its log itself and can be read afterwards, which removes the
connection from the experiment entirely.
Each rate is judged on the lane-entry beams, not on delivery alone: an item left on the belt
and an item thrown to the floor both score zero and need opposite corrections.
crossed the push reached the lane -> low means the sweep is too slow
speed how fast it crossed -> high means it is throwing them
homed completed returns to centre -> the blade must park in the middle between items
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
def parse_args(argv=None):
p = argparse.ArgumentParser(description="tune the plow sweep rate")
p.add_argument("--rates", default="120,200,300,450", help="deg/s, comma separated")
p.add_argument("--items", type=int, default=4, help="how many B/C items per rate")
p.add_argument("--speed", type=float, default=0.8, help="belt m/s")
p.add_argument("--pitch", type=float, default=3.5, help="metres between items")
p.add_argument("--seconds", type=float, default=60.0, help="budget per rate")
p.add_argument("--headless", action="store_true")
p.add_argument("--out", default=str(ROOT / "runs" / "sweep_tuning.json"))
return p.parse_args(argv)
async def _run(app_utils, args):
import omni.timeline
from omni.physx import get_physx_interface
from robozon_sorter import config as C
from robozon_sorter.sim import lane_beams, plow_sort, plow_vision, staging
from robozon_sorter.sim.mechanics import Cell
from robozon_sorter.sim.spawner import AutoFeeder
rates = [float(r) for r in args.rates.split(",") if r.strip()]
stage, info = plow_vision.load(belt_speed=args.speed, script_control=True,
meshes_dir=str(ROOT / "assets" / "items"))
staging.stage_cell(stage, preset="bright", floor=True)
plow_sort.keep_lanes_active(stage)
lanes = plow_sort.configure_lanes(stage, args.speed)
plow_sort.open_junction(stage)
items = {k: v["zone"] for k, v in info["items"].items()}
print(f"scene ready: {len(items)} items, {len(lanes)} lanes/decks driven")
await app_utils.update_app_async(steps=40)
cell = Cell(stage, items.keys())
order = [n for n in sorted(items) if items[n] in ("B", "C")][:args.items]
want = {"B": "container_B", "C": "container_C"}
print(f"tuning on {len(order)} B/C items: {order}")
timeline = omni.timeline.get_timeline_interface()
results = []
for rate in rates:
C.PLOW_SWEEP_RATE = rate
cell.park_all()
await app_utils.update_app_async(steps=20)
sorter = plow_sort.PlowSorter(stage, cell, items, plow_sort.calibrate_mapping())
beams = lane_beams.LaneBeams(stage, cell, plow=sorter.plow)
def _step(dt, _s=sorter, _b=beams, _r=rate):
try:
_s.update(dt)
_b.tick(dt)
_b.poll(rate=_r)
except Exception:
pass
sub = get_physx_interface().subscribe_physics_step_events(_step)
feeder = AutoFeeder(cell, order=order, pitch=args.pitch, route={}).install()
app_utils.play(commit=True)
await app_utils.update_app_async(steps=20)
for _ in range(int(args.seconds * 4)):
await app_utils.update_app_async(steps=15)
if len(beams.crossings) >= len(order):
break
app_utils.stop()
await app_utils.update_app_async(steps=15)
feeder.remove()
sub = None
cr = beams.report()
sp = [c["speed"] for c in cr]
where = {n: sorter.lane_of(n) for n in order}
delivered = sum(1 for n in order if where[n] == want[items[n]])
row = dict(rate=rate, crossed=len(cr), of=len(order), delivered=delivered,
homed=sorter.homed,
mean_cross_speed=round(sum(sp) / len(sp), 2) if sp else None,
max_cross_speed=round(max(sp), 2) if sp else None,
where=where, crossings=cr)
results.append(row)
print(f" rate {rate:6.0f} deg/s (tip {C.PLOW_ARM_LEN * rate * 3.14159 / 180:.2f} m/s)"
f" -> crossed {len(cr)}/{len(order)} delivered {delivered} "
f"homed {sorter.homed} cross v mean {row['mean_cross_speed']} "
f"max {row['max_cross_speed']}")
print("\n===== SWEEP RATE TUNING =====")
print(f"{'rate':>7} {'tip m/s':>8} {'crossed':>9} {'delivered':>10} {'homed':>6} "
f"{'mean v':>8} {'max v':>7}")
for r in results:
print(f"{r['rate']:7.0f} {0.6 * r['rate'] * 3.14159 / 180:8.2f} "
f"{r['crossed']:4d}/{r['of']:<4d} {r['delivered']:10d} {r['homed']:6d} "
f"{str(r['mean_cross_speed']):>8} {str(r['max_cross_speed']):>7}")
if results:
best = max(results, key=lambda r: (r["delivered"], r["crossed"],
-(r["max_cross_speed"] or 99)))
print(f"\nbest: {best['rate']:.0f} deg/s "
f"crossed {best['crossed']}/{best['of']} delivered {best['delivered']}")
out = Path(args.out)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(json.dumps(dict(rates=rates, belt=args.speed, pitch=args.pitch,
items=order, results=results), indent=2))
print(f"log -> {out}")
return results
def main(argv=None):
args = parse_args(argv)
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from isaacsim import SimulationApp
app = SimulationApp({"headless": args.headless, "width": 1280, "height": 800})
try:
import asyncio
import isaacsim.core.experimental.utils.app as app_utils
return asyncio.get_event_loop().run_until_complete(_run(app_utils, args))
finally:
app.close()
if __name__ == "__main__":
sys.exit(0 if main() else 1)