Commit ·
33523d4
1
Parent(s): 675f070
Fix: Self-contained graders and spec compliance
Browse files- core/graders.py +40 -20
- data/memory.json +54 -50
- openenv.yaml +0 -5
- server/app.py +29 -6
- server/grid_world_environment.py +17 -7
core/graders.py
CHANGED
|
@@ -1,49 +1,69 @@
|
|
| 1 |
"""
|
| 2 |
drone_delivery_env/core/graders.py
|
| 3 |
Evaluation logic for scoring Drone Delivery missions.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
"""
|
| 5 |
-
from drone_env.models import DroneState
|
| 6 |
|
| 7 |
|
| 8 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
"""
|
| 10 |
Unified grade calculation:
|
| 11 |
-
-
|
| 12 |
-
-
|
|
|
|
| 13 |
"""
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
# Efficiency factor (0.0 to 1.0)
|
| 19 |
-
|
| 20 |
-
efficiency = state.battery * 0.5 + (1.0 - (state.step_count / max_steps)) * 0.5
|
| 21 |
efficiency = max(0.0, min(1.0, efficiency))
|
| 22 |
-
|
| 23 |
score = (delivery_ratio * 0.8) + (efficiency * 0.2)
|
| 24 |
-
|
| 25 |
-
#
|
| 26 |
-
if
|
| 27 |
score = min(score, 0.49)
|
| 28 |
-
|
| 29 |
# Hackathon Requirement: Score must be strictly between 0.0 and 1.0
|
| 30 |
return max(0.01, min(0.99, float(score)))
|
| 31 |
|
| 32 |
|
| 33 |
-
def grade_easy(state
|
|
|
|
| 34 |
return compute_grade(state, 60.0)
|
| 35 |
|
| 36 |
|
| 37 |
-
def grade_medium(state
|
|
|
|
| 38 |
return compute_grade(state, 100.0)
|
| 39 |
|
| 40 |
|
| 41 |
-
def grade_hard(state
|
|
|
|
| 42 |
return compute_grade(state, 160.0)
|
| 43 |
|
| 44 |
|
| 45 |
GRADERS = {
|
| 46 |
-
"drone_env.core.graders:grade_easy":
|
| 47 |
"drone_env.core.graders:grade_medium": grade_medium,
|
| 48 |
-
"drone_env.core.graders:grade_hard":
|
| 49 |
}
|
|
|
|
| 1 |
"""
|
| 2 |
drone_delivery_env/core/graders.py
|
| 3 |
Evaluation logic for scoring Drone Delivery missions.
|
| 4 |
+
|
| 5 |
+
NOTE: This module is intentionally self-contained with NO external package imports.
|
| 6 |
+
The OpenEnv validator imports grader functions directly via the module path
|
| 7 |
+
(e.g. drone_env.core.graders:grade_easy). Any import failure here means the
|
| 8 |
+
validator counts 0 working graders — causing "Not enough tasks with graders" error.
|
| 9 |
"""
|
|
|
|
| 10 |
|
| 11 |
|
| 12 |
+
def _get_attr(state, key, default=0):
|
| 13 |
+
"""Safely get an attribute from a state object or dict."""
|
| 14 |
+
if isinstance(state, dict):
|
| 15 |
+
return state.get(key, default)
|
| 16 |
+
return getattr(state, key, default)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def compute_grade(state, max_steps: float) -> float:
|
| 20 |
"""
|
| 21 |
Unified grade calculation:
|
| 22 |
+
- 80% weighted by deliveries completed.
|
| 23 |
+
- 20% weighted by efficiency (remaining battery and steps).
|
| 24 |
+
Accepts any object or dict with the required fields.
|
| 25 |
"""
|
| 26 |
+
deliveries_total = _get_attr(state, "deliveries_total", 0)
|
| 27 |
+
deliveries_done = _get_attr(state, "deliveries_done", 0)
|
| 28 |
+
battery = _get_attr(state, "battery", 1.0)
|
| 29 |
+
step_count = _get_attr(state, "step_count", 0)
|
| 30 |
+
|
| 31 |
+
if deliveries_total == 0:
|
| 32 |
+
return 0.5 # neutral score when no deliveries configured
|
| 33 |
+
|
| 34 |
+
delivery_ratio = deliveries_done / deliveries_total
|
| 35 |
+
|
| 36 |
# Efficiency factor (0.0 to 1.0)
|
| 37 |
+
efficiency = float(battery) * 0.5 + (1.0 - (float(step_count) / float(max_steps))) * 0.5
|
|
|
|
| 38 |
efficiency = max(0.0, min(1.0, efficiency))
|
| 39 |
+
|
| 40 |
score = (delivery_ratio * 0.8) + (efficiency * 0.2)
|
| 41 |
+
|
| 42 |
+
# Cap score if not all deliveries are done
|
| 43 |
+
if deliveries_done < deliveries_total:
|
| 44 |
score = min(score, 0.49)
|
| 45 |
+
|
| 46 |
# Hackathon Requirement: Score must be strictly between 0.0 and 1.0
|
| 47 |
return max(0.01, min(0.99, float(score)))
|
| 48 |
|
| 49 |
|
| 50 |
+
def grade_easy(state) -> float:
|
| 51 |
+
"""Grader for easy_delivery task (10x10 grid, 1 delivery, 60 max steps)."""
|
| 52 |
return compute_grade(state, 60.0)
|
| 53 |
|
| 54 |
|
| 55 |
+
def grade_medium(state) -> float:
|
| 56 |
+
"""Grader for medium_delivery task (14x14 grid, 3 deliveries, 100 max steps)."""
|
| 57 |
return compute_grade(state, 100.0)
|
| 58 |
|
| 59 |
|
| 60 |
+
def grade_hard(state) -> float:
|
| 61 |
+
"""Grader for hard_delivery task (18x18 grid, 5 deliveries, 160 max steps)."""
|
| 62 |
return compute_grade(state, 160.0)
|
| 63 |
|
| 64 |
|
| 65 |
GRADERS = {
|
| 66 |
+
"drone_env.core.graders:grade_easy": grade_easy,
|
| 67 |
"drone_env.core.graders:grade_medium": grade_medium,
|
| 68 |
+
"drone_env.core.graders:grade_hard": grade_hard,
|
| 69 |
}
|
data/memory.json
CHANGED
|
@@ -1,54 +1,4 @@
|
|
| 1 |
[
|
| 2 |
-
{
|
| 3 |
-
"task": "drone_env.core.graders:grade_easy",
|
| 4 |
-
"steps": [
|
| 5 |
-
{
|
| 6 |
-
"step": 1,
|
| 7 |
-
"x": 1,
|
| 8 |
-
"y": 1,
|
| 9 |
-
"action": "RIGHT",
|
| 10 |
-
"reward": -0.05,
|
| 11 |
-
"battery": 0.9833
|
| 12 |
-
},
|
| 13 |
-
{
|
| 14 |
-
"step": 2,
|
| 15 |
-
"x": 2,
|
| 16 |
-
"y": 1,
|
| 17 |
-
"action": "RIGHT",
|
| 18 |
-
"reward": -0.1,
|
| 19 |
-
"battery": 0.9667
|
| 20 |
-
},
|
| 21 |
-
{
|
| 22 |
-
"step": 3,
|
| 23 |
-
"x": 2,
|
| 24 |
-
"y": 0,
|
| 25 |
-
"action": "UP",
|
| 26 |
-
"reward": -0.05,
|
| 27 |
-
"battery": 0.95
|
| 28 |
-
},
|
| 29 |
-
{
|
| 30 |
-
"step": 4,
|
| 31 |
-
"x": 3,
|
| 32 |
-
"y": 0,
|
| 33 |
-
"action": "RIGHT",
|
| 34 |
-
"reward": 0.95,
|
| 35 |
-
"battery": 0.9333
|
| 36 |
-
}
|
| 37 |
-
],
|
| 38 |
-
"grid_meta": {
|
| 39 |
-
"width": 10,
|
| 40 |
-
"height": 10
|
| 41 |
-
},
|
| 42 |
-
"delivery_positions": [
|
| 43 |
-
[
|
| 44 |
-
3,
|
| 45 |
-
0
|
| 46 |
-
]
|
| 47 |
-
],
|
| 48 |
-
"deliveries_done": 1,
|
| 49 |
-
"total_reward": 0.75,
|
| 50 |
-
"total_steps": 4
|
| 51 |
-
},
|
| 52 |
{
|
| 53 |
"task": "drone_env.core.graders:grade_easy",
|
| 54 |
"steps": [
|
|
@@ -17940,5 +17890,59 @@
|
|
| 17940 |
"deliveries_done": 3,
|
| 17941 |
"total_reward": 5.9,
|
| 17942 |
"total_steps": 20
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17943 |
}
|
| 17944 |
]
|
|
|
|
| 1 |
[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
{
|
| 3 |
"task": "drone_env.core.graders:grade_easy",
|
| 4 |
"steps": [
|
|
|
|
| 17890 |
"deliveries_done": 3,
|
| 17891 |
"total_reward": 5.9,
|
| 17892 |
"total_steps": 20
|
| 17893 |
+
},
|
| 17894 |
+
{
|
| 17895 |
+
"task": "drone_env.core.graders:grade_easy",
|
| 17896 |
+
"steps": [
|
| 17897 |
+
{
|
| 17898 |
+
"step": 1,
|
| 17899 |
+
"x": 4,
|
| 17900 |
+
"y": 2,
|
| 17901 |
+
"action": "LEFT",
|
| 17902 |
+
"reward": 0.1,
|
| 17903 |
+
"battery": 0.9833,
|
| 17904 |
+
"message": "On road \ud83d\udee3\ufe0f"
|
| 17905 |
+
},
|
| 17906 |
+
{
|
| 17907 |
+
"step": 2,
|
| 17908 |
+
"x": 3,
|
| 17909 |
+
"y": 2,
|
| 17910 |
+
"action": "LEFT",
|
| 17911 |
+
"reward": 0.1,
|
| 17912 |
+
"battery": 0.9667,
|
| 17913 |
+
"message": "On road \ud83d\udee3\ufe0f"
|
| 17914 |
+
},
|
| 17915 |
+
{
|
| 17916 |
+
"step": 3,
|
| 17917 |
+
"x": 3,
|
| 17918 |
+
"y": 1,
|
| 17919 |
+
"action": "UP",
|
| 17920 |
+
"reward": 0.1,
|
| 17921 |
+
"battery": 0.95,
|
| 17922 |
+
"message": "On road \ud83d\udee3\ufe0f"
|
| 17923 |
+
},
|
| 17924 |
+
{
|
| 17925 |
+
"step": 4,
|
| 17926 |
+
"x": 2,
|
| 17927 |
+
"y": 1,
|
| 17928 |
+
"action": "LEFT",
|
| 17929 |
+
"reward": 1.05,
|
| 17930 |
+
"battery": 0.9333,
|
| 17931 |
+
"message": "\u2705 Delivery 1/1 done!"
|
| 17932 |
+
}
|
| 17933 |
+
],
|
| 17934 |
+
"grid_meta": {
|
| 17935 |
+
"width": 10,
|
| 17936 |
+
"height": 10
|
| 17937 |
+
},
|
| 17938 |
+
"delivery_positions": [
|
| 17939 |
+
[
|
| 17940 |
+
2,
|
| 17941 |
+
1
|
| 17942 |
+
]
|
| 17943 |
+
],
|
| 17944 |
+
"deliveries_done": 1,
|
| 17945 |
+
"total_reward": 1.35,
|
| 17946 |
+
"total_steps": 4
|
| 17947 |
}
|
| 17948 |
]
|
openenv.yaml
CHANGED
|
@@ -11,8 +11,3 @@ tasks:
|
|
| 11 |
grader: drone_env.core.graders:grade_medium
|
| 12 |
- id: hard_delivery
|
| 13 |
grader: drone_env.core.graders:grade_hard
|
| 14 |
-
graders:
|
| 15 |
-
- id: drone_env.core.graders:grade_easy
|
| 16 |
-
- id: drone_env.core.graders:grade_medium
|
| 17 |
-
- id: drone_env.core.graders:grade_hard
|
| 18 |
-
|
|
|
|
| 11 |
grader: drone_env.core.graders:grade_medium
|
| 12 |
- id: hard_delivery
|
| 13 |
grader: drone_env.core.graders:grade_hard
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/app.py
CHANGED
|
@@ -90,17 +90,27 @@ async def step(action: DroneAction):
|
|
| 90 |
async def get_state():
|
| 91 |
return _env.state
|
| 92 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
@app.get("/grade/{task_name}")
|
| 94 |
async def grade(task_name: str):
|
| 95 |
-
|
|
|
|
|
|
|
| 96 |
raise HTTPException(404, detail=f"Unknown task: {task_name}")
|
| 97 |
-
return {"task": task_name, "score": float(GRADERS[
|
| 98 |
|
| 99 |
@app.get("/analyse/{task_name}")
|
| 100 |
async def analyse(task_name: str):
|
| 101 |
-
|
|
|
|
| 102 |
raise HTTPException(404, detail=f"Unknown task: {task_name}")
|
| 103 |
-
return PathLearner.analyse_episodes(
|
| 104 |
|
| 105 |
@app.get("/path_history")
|
| 106 |
async def path_history():
|
|
@@ -108,11 +118,24 @@ async def path_history():
|
|
| 108 |
|
| 109 |
@app.get("/tasks")
|
| 110 |
async def list_tasks():
|
| 111 |
-
return {"tasks": [{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
@app.get("/graders")
|
| 114 |
async def list_graders():
|
| 115 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
|
| 117 |
@app.get("/logs")
|
| 118 |
async def get_logs():
|
|
|
|
| 90 |
async def get_state():
|
| 91 |
return _env.state
|
| 92 |
|
| 93 |
+
# Task ID → grader key mapping (matches openenv.yaml task ids)
|
| 94 |
+
TASK_ID_TO_GRADER = {
|
| 95 |
+
"easy_delivery": "drone_env.core.graders:grade_easy",
|
| 96 |
+
"medium_delivery": "drone_env.core.graders:grade_medium",
|
| 97 |
+
"hard_delivery": "drone_env.core.graders:grade_hard",
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
@app.get("/grade/{task_name}")
|
| 101 |
async def grade(task_name: str):
|
| 102 |
+
# Accept both short task IDs ("easy_delivery") and full grader keys
|
| 103 |
+
grader_key = TASK_ID_TO_GRADER.get(task_name, task_name)
|
| 104 |
+
if grader_key not in GRADERS:
|
| 105 |
raise HTTPException(404, detail=f"Unknown task: {task_name}")
|
| 106 |
+
return {"task": task_name, "score": float(GRADERS[grader_key](_env.state))}
|
| 107 |
|
| 108 |
@app.get("/analyse/{task_name}")
|
| 109 |
async def analyse(task_name: str):
|
| 110 |
+
grader_key = TASK_ID_TO_GRADER.get(task_name, task_name)
|
| 111 |
+
if grader_key not in TASK_CONFIG:
|
| 112 |
raise HTTPException(404, detail=f"Unknown task: {task_name}")
|
| 113 |
+
return PathLearner.analyse_episodes(grader_key)
|
| 114 |
|
| 115 |
@app.get("/path_history")
|
| 116 |
async def path_history():
|
|
|
|
| 118 |
|
| 119 |
@app.get("/tasks")
|
| 120 |
async def list_tasks():
|
| 121 |
+
return {"tasks": [{
|
| 122 |
+
"id": tid,
|
| 123 |
+
"name": tid,
|
| 124 |
+
"grader": gkey,
|
| 125 |
+
**TASK_CONFIG[gkey]
|
| 126 |
+
} for tid, gkey in TASK_ID_TO_GRADER.items()]}
|
| 127 |
|
| 128 |
@app.get("/graders")
|
| 129 |
async def list_graders():
|
| 130 |
+
# Return task IDs (the submission system expects these to match openenv.yaml task ids)
|
| 131 |
+
return {"graders": list(TASK_ID_TO_GRADER.keys())}
|
| 132 |
+
|
| 133 |
+
@app.post("/grade/{task_name}")
|
| 134 |
+
async def grade_post(task_name: str):
|
| 135 |
+
grader_key = TASK_ID_TO_GRADER.get(task_name, task_name)
|
| 136 |
+
if grader_key not in GRADERS:
|
| 137 |
+
raise HTTPException(404, detail=f"Unknown task: {task_name}")
|
| 138 |
+
return {"task": task_name, "score": float(GRADERS[grader_key](_env.state))}
|
| 139 |
|
| 140 |
@app.get("/logs")
|
| 141 |
async def get_logs():
|
server/grid_world_environment.py
CHANGED
|
@@ -49,14 +49,24 @@ class DroneDeliveryEnvironment(Environment):
|
|
| 49 |
if self._step_records and self._state.episode_id and not self._state.done:
|
| 50 |
self._persist_episode()
|
| 51 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
task = "drone_env.core.graders:grade_easy"
|
| 53 |
-
if action and action.task_name
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
self._cfg = TASK_CONFIG[task]
|
| 62 |
self._rng.manual_seed(random.randint(0, 2**31))
|
|
|
|
| 49 |
if self._step_records and self._state.episode_id and not self._state.done:
|
| 50 |
self._persist_episode()
|
| 51 |
|
| 52 |
+
# Map short task IDs (from openenv.yaml) to full grader keys
|
| 53 |
+
_TASK_ID_MAP = {
|
| 54 |
+
"easy_delivery": "drone_env.core.graders:grade_easy",
|
| 55 |
+
"medium_delivery": "drone_env.core.graders:grade_medium",
|
| 56 |
+
"hard_delivery": "drone_env.core.graders:grade_hard",
|
| 57 |
+
}
|
| 58 |
task = "drone_env.core.graders:grade_easy"
|
| 59 |
+
if action and action.task_name:
|
| 60 |
+
name = action.task_name
|
| 61 |
+
if name in _TASK_ID_MAP:
|
| 62 |
+
task = _TASK_ID_MAP[name]
|
| 63 |
+
elif name in TASK_CONFIG:
|
| 64 |
+
task = name
|
| 65 |
+
else:
|
| 66 |
+
# handle partial matches like "easy"
|
| 67 |
+
for k in TASK_CONFIG:
|
| 68 |
+
if k.startswith(name):
|
| 69 |
+
task = k; break
|
| 70 |
|
| 71 |
self._cfg = TASK_CONFIG[task]
|
| 72 |
self._rng.manual_seed(random.randint(0, 2**31))
|