Spaces:
Running
Running
Deploy portal from RoboEval corl2026-challenge
Browse files- Dockerfile +1 -1
- app.py +5 -3
- portal_core.py +20 -8
- test_portal.py +14 -1
- test_scoring.py +42 -0
Dockerfile
CHANGED
|
@@ -28,7 +28,7 @@ WORKDIR /home/user
|
|
| 28 |
# otherwise reuse a stale clone) and fails the build if the tag has moved.
|
| 29 |
ARG ROBOEVAL_REPO=https://github.com/Robo-Eval/RoboEval.git
|
| 30 |
ARG ROBOEVAL_REF=corl2026-challenge
|
| 31 |
-
ARG ROBOEVAL_SHA=
|
| 32 |
RUN git clone --branch "$ROBOEVAL_REF" "$ROBOEVAL_REPO" RoboEval \
|
| 33 |
&& if [ -n "$ROBOEVAL_SHA" ]; then test "$(git -C RoboEval rev-parse HEAD)" = "$ROBOEVAL_SHA"; fi \
|
| 34 |
&& git -C RoboEval submodule update --init \
|
|
|
|
| 28 |
# otherwise reuse a stale clone) and fails the build if the tag has moved.
|
| 29 |
ARG ROBOEVAL_REPO=https://github.com/Robo-Eval/RoboEval.git
|
| 30 |
ARG ROBOEVAL_REF=corl2026-challenge
|
| 31 |
+
ARG ROBOEVAL_SHA=c8b79598c9fb9021652128020517d1847abe4c95
|
| 32 |
RUN git clone --branch "$ROBOEVAL_REF" "$ROBOEVAL_REPO" RoboEval \
|
| 33 |
&& if [ -n "$ROBOEVAL_SHA" ]; then test "$(git -C RoboEval rev-parse HEAD)" = "$ROBOEVAL_SHA"; fi \
|
| 34 |
&& git -C RoboEval submodule update --init \
|
app.py
CHANGED
|
@@ -159,9 +159,11 @@ with gr.Blocks(title="RoboEval Challenge Β· CoRL 2026") as demo:
|
|
| 159 |
"[Submission guidelines](https://robo-eval.github.io/challenge-submission.html)"
|
| 160 |
)
|
| 161 |
with gr.Tab("Leaderboard"):
|
| 162 |
-
gr.Markdown("Each team's most recent accepted submission, ranked by
|
| 163 |
-
"
|
| 164 |
-
"
|
|
|
|
|
|
|
| 165 |
with gr.Row():
|
| 166 |
task = gr.Dropdown(TASK_CHOICES, value="overall", label="Task")
|
| 167 |
family = gr.Radio(FAMILY_CHOICES, value="All metrics", label="Metrics")
|
|
|
|
| 159 |
"[Submission guidelines](https://robo-eval.github.io/challenge-submission.html)"
|
| 160 |
)
|
| 161 |
with gr.Tab("Leaderboard"):
|
| 162 |
+
gr.Markdown("Each team's most recent accepted submission, ranked by the RoboEval Diagnostic Score (**SCORE**: "
|
| 163 |
+
"40% outcome, 30% safety & stability, 20% efficiency, 10% coordination; "
|
| 164 |
+
"[how it is computed](https://robo-eval.github.io/challenge-rules.html#ranking)), then Success, "
|
| 165 |
+
"Task Progression, and submission time. β higher is better, β lower is better. Every entry runs "
|
| 166 |
+
"at 20 Hz; **Action** is the team's declared action mode.")
|
| 167 |
with gr.Row():
|
| 168 |
task = gr.Dropdown(TASK_CHOICES, value="overall", label="Task")
|
| 169 |
family = gr.Radio(FAMILY_CHOICES, value="All metrics", label="Metrics")
|
portal_core.py
CHANGED
|
@@ -23,7 +23,7 @@ from typing import Any, Dict, List, Optional, Tuple
|
|
| 23 |
import requests
|
| 24 |
import yaml
|
| 25 |
|
| 26 |
-
from roboeval.challenge import spec
|
| 27 |
from roboeval.challenge.package import MANIFEST
|
| 28 |
from roboeval.challenge.validate import validate
|
| 29 |
|
|
@@ -457,6 +457,16 @@ def action_label(record: Dict[str, Any]) -> str:
|
|
| 457 |
return f"{'EE' if mode.get('ee') else 'Joint'} {'absolute' if mode.get('absolute') else 'delta'}"
|
| 458 |
|
| 459 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 460 |
def task_metrics(record: Dict[str, Any], task: Optional[str]) -> Dict[str, float]:
|
| 461 |
"""Suite means (task=None) or one task family's means for a submission."""
|
| 462 |
summary = record["summary"]
|
|
@@ -464,18 +474,18 @@ def task_metrics(record: Dict[str, Any], task: Optional[str]) -> Dict[str, float
|
|
| 464 |
|
| 465 |
|
| 466 |
def leaderboard(records: List[Dict[str, Any]], task: Optional[str] = None) -> List[Dict[str, Any]]:
|
| 467 |
-
"""Most recent accepted submission per team,
|
| 468 |
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
the whole suite or over one task family.
|
| 472 |
"""
|
| 473 |
latest: Dict[str, Dict[str, Any]] = {}
|
| 474 |
for r in sorted(records, key=lambda r: r["created_utc"]):
|
| 475 |
if r["status"] in RANKED_STATUSES:
|
| 476 |
latest[r["team_name"].casefold()] = r
|
| 477 |
rows = [r for r in latest.values() if task is None or task in r["summary"].get("per_family", {})]
|
| 478 |
-
rows.sort(key=lambda r: (-
|
|
|
|
| 479 |
return rows
|
| 480 |
|
| 481 |
|
|
@@ -490,9 +500,11 @@ def leaderboard_table(records: List[Dict[str, Any]], observation_mode: Optional[
|
|
| 490 |
continue
|
| 491 |
if action_mode and action_label(r) != action_mode:
|
| 492 |
continue
|
| 493 |
-
m = task_metrics(r, task)
|
| 494 |
row = {"Rank": len(out) + 1, "Team": r["team_name"], "Method": r["method_name"],
|
| 495 |
-
"Obs.": r["observation_mode"], "Action": action_label(r)}
|
|
|
|
|
|
|
| 496 |
row.update({f"{k.upper()} {'β' if k in HIGHER_IS_BETTER else 'β'}": round(m[k], 3) for k in keys})
|
| 497 |
row["Open source"] = "yes" if r["open_source"] else ""
|
| 498 |
row["Note"] = "organizer (not prize-eligible)" if r["organizer"] else ""
|
|
|
|
| 23 |
import requests
|
| 24 |
import yaml
|
| 25 |
|
| 26 |
+
from roboeval.challenge import scoring, spec
|
| 27 |
from roboeval.challenge.package import MANIFEST
|
| 28 |
from roboeval.challenge.validate import validate
|
| 29 |
|
|
|
|
| 457 |
return f"{'EE' if mode.get('ee') else 'Joint'} {'absolute' if mode.get('absolute') else 'delta'}"
|
| 458 |
|
| 459 |
|
| 460 |
+
FAMILY_SCORE_KEYS = {"Outcome": "outcome", "Efficiency": "efficiency", "Coordination": "coordination",
|
| 461 |
+
"Safety & Stability": "safety"}
|
| 462 |
+
|
| 463 |
+
|
| 464 |
+
def record_scores(record: Dict[str, Any], task: Optional[str]) -> Dict[str, float]:
|
| 465 |
+
"""RDS and family scores for a submission, over the suite or one task."""
|
| 466 |
+
scores = scoring.suite_scores(record["summary"]["per_family"])
|
| 467 |
+
return scores["overall"] if task is None else scores["tasks"][task]
|
| 468 |
+
|
| 469 |
+
|
| 470 |
def task_metrics(record: Dict[str, Any], task: Optional[str]) -> Dict[str, float]:
|
| 471 |
"""Suite means (task=None) or one task family's means for a submission."""
|
| 472 |
summary = record["summary"]
|
|
|
|
| 474 |
|
| 475 |
|
| 476 |
def leaderboard(records: List[Dict[str, Any]], task: Optional[str] = None) -> List[Dict[str, Any]]:
|
| 477 |
+
"""Most recent accepted submission per team, over the suite or one task.
|
| 478 |
|
| 479 |
+
Ranked by the RoboEval Diagnostic Score, then Success, Task Progression,
|
| 480 |
+
and earlier submission, as the rules' tie-break order specifies.
|
|
|
|
| 481 |
"""
|
| 482 |
latest: Dict[str, Dict[str, Any]] = {}
|
| 483 |
for r in sorted(records, key=lambda r: r["created_utc"]):
|
| 484 |
if r["status"] in RANKED_STATUSES:
|
| 485 |
latest[r["team_name"].casefold()] = r
|
| 486 |
rows = [r for r in latest.values() if task is None or task in r["summary"].get("per_family", {})]
|
| 487 |
+
rows.sort(key=lambda r: (-record_scores(r, task)["score"], -task_metrics(r, task)["success"],
|
| 488 |
+
-task_metrics(r, task)["tp"], -record_scores(r, task)["safety"], r["created_utc"]))
|
| 489 |
return rows
|
| 490 |
|
| 491 |
|
|
|
|
| 500 |
continue
|
| 501 |
if action_mode and action_label(r) != action_mode:
|
| 502 |
continue
|
| 503 |
+
m, sc = task_metrics(r, task), record_scores(r, task)
|
| 504 |
row = {"Rank": len(out) + 1, "Team": r["team_name"], "Method": r["method_name"],
|
| 505 |
+
"Obs.": r["observation_mode"], "Action": action_label(r), "SCORE β": round(sc["score"], 3)}
|
| 506 |
+
if family:
|
| 507 |
+
row[f"{family.upper()} SCORE β"] = round(sc[FAMILY_SCORE_KEYS[family]], 3)
|
| 508 |
row.update({f"{k.upper()} {'β' if k in HIGHER_IS_BETTER else 'β'}": round(m[k], 3) for k in keys})
|
| 509 |
row["Open source"] = "yes" if r["open_source"] else ""
|
| 510 |
row["Note"] = "organizer (not prize-eligible)" if r["organizer"] else ""
|
test_portal.py
CHANGED
|
@@ -94,6 +94,19 @@ def test_edited_file_breaks_manifest(cfg, store, tmp_path):
|
|
| 94 |
core.process_submission(cfg, store, "alice", str(bad), sha(bad))
|
| 95 |
|
| 96 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
def test_edited_results_with_regenerated_manifest_is_rejected(cfg, store, tmp_path):
|
| 98 |
def inflate(b):
|
| 99 |
r = json.loads(b)
|
|
@@ -206,5 +219,5 @@ def test_leaderboard_views(cfg, store):
|
|
| 206 |
per_task = core.leaderboard_table(records, task="LiftPot")
|
| 207 |
assert per_task[0]["TL β"] == round(records[0]["summary"]["per_family"]["LiftPot"]["tl"], 3)
|
| 208 |
coordination = core.leaderboard_table(records, family="Coordination")
|
| 209 |
-
assert {k for k in coordination[0] if "β" in k or "β" in k} == {"BAVD β", "BGVD β"}
|
| 210 |
assert core.leaderboard_table(records, action_mode="Joint delta") == []
|
|
|
|
| 94 |
core.process_submission(cfg, store, "alice", str(bad), sha(bad))
|
| 95 |
|
| 96 |
|
| 97 |
+
def test_edited_summary_is_rejected(cfg, store, tmp_path):
|
| 98 |
+
def inflate(b):
|
| 99 |
+
r = json.loads(b)
|
| 100 |
+
r["summary"]["suite"]["success"] = 0.99
|
| 101 |
+
return json.dumps(r).encode()
|
| 102 |
+
|
| 103 |
+
bad = repack(Path(BUNDLE), tmp_path / "bad.tar.gz", edit={"submission/results.json": inflate},
|
| 104 |
+
rewrite_manifest=True)
|
| 105 |
+
out = core.process_submission(cfg, store, "alice", str(bad), sha(bad))
|
| 106 |
+
assert out.record["status"] == core.REJECTED
|
| 107 |
+
assert any(i["check"] == "consistency" and "summary" in i["message"] for i in out.report)
|
| 108 |
+
|
| 109 |
+
|
| 110 |
def test_edited_results_with_regenerated_manifest_is_rejected(cfg, store, tmp_path):
|
| 111 |
def inflate(b):
|
| 112 |
r = json.loads(b)
|
|
|
|
| 219 |
per_task = core.leaderboard_table(records, task="LiftPot")
|
| 220 |
assert per_task[0]["TL β"] == round(records[0]["summary"]["per_family"]["LiftPot"]["tl"], 3)
|
| 221 |
coordination = core.leaderboard_table(records, family="Coordination")
|
| 222 |
+
assert {k for k in coordination[0] if "β" in k or "β" in k} == {"SCORE β", "COORDINATION SCORE β", "BAVD β", "BGVD β"}
|
| 223 |
assert core.leaderboard_table(records, action_mode="Joint delta") == []
|
test_scoring.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Unit tests for the RoboEval Diagnostic Score (no simulator or bundle needed)."""
|
| 2 |
+
import pytest
|
| 3 |
+
|
| 4 |
+
from roboeval.challenge import scoring, spec
|
| 5 |
+
|
| 6 |
+
REF = {"success": 1.0, "tp": 1.0, "cpl": 2.0, "jpl": 4.0, "opl": 3.0, "ct": 8.0, "tl": 160.0,
|
| 7 |
+
"bavd": 0.2, "bgvd": 0.05, "scc": 0.0, "ecc": 1.0, "sc": 0.0, "mjj": 20.0, "mcj": 5.0}
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@pytest.fixture(autouse=True)
|
| 11 |
+
def reference(monkeypatch):
|
| 12 |
+
monkeypatch.setattr(scoring, "human_reference", lambda: {f.key: dict(REF) for f in spec.TASK_FAMILIES})
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def test_doing_nothing_scores_zero():
|
| 16 |
+
idle = {k: 0.0 for k in REF} # no motion, no contacts, no success
|
| 17 |
+
assert scoring.task_scores("LiftPot", idle)["score"] == 0.0
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def test_human_level_success_scores_one():
|
| 21 |
+
assert scoring.task_scores("LiftPot", REF)["score"] == pytest.approx(1.0)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def test_worse_behavior_lowers_score_smoothly():
|
| 25 |
+
m = dict(REF, cpl=4.0) # twice the human path length
|
| 26 |
+
assert scoring.metric_score("cpl", 4.0, 2.0) == pytest.approx(2.2 / 4.2)
|
| 27 |
+
assert scoring.task_scores("LiftPot", m)["efficiency"] < 1.0
|
| 28 |
+
assert scoring.task_scores("LiftPot", m)["score"] < 1.0
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def test_count_metric_with_zero_reference():
|
| 32 |
+
assert scoring.metric_score("scc", 1.0, 0.0) == pytest.approx(0.5)
|
| 33 |
+
assert scoring.metric_score("scc", 3.0, 0.0) == pytest.approx(0.25)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def test_outcome_weighting_and_suite_mean():
|
| 37 |
+
half = dict(REF, success=0.5, tp=0.5)
|
| 38 |
+
t = scoring.task_scores("LiftPot", half)
|
| 39 |
+
assert t["outcome"] == pytest.approx(0.5)
|
| 40 |
+
assert t["score"] == pytest.approx(0.4 * 0.5 + 0.5 * 0.6)
|
| 41 |
+
s = scoring.suite_scores({"LiftPot": REF, "PackBox": half})
|
| 42 |
+
assert s["overall"]["score"] == pytest.approx((1.0 + t["score"]) / 2)
|